From 07da528ce2223582f84bf64d2fec69714c647ddc Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Sat, 19 Aug 2023 01:59:57 +0200 Subject: [PATCH] refactor!: restructure code This commit focuses on improving code quality in a couple of places and bumping the dependency to ReVanced Patcher. BREAKING CHANGE: This introduces major changes to how ReVanced CLI is used from the command line. --- build.gradle.kts | 4 +- .../app/revanced/cli/command/MainCommand.kt | 387 +++++++++++------- .../app/revanced/cli/patcher/Patcher.kt | 23 -- src/main/kotlin/app/revanced/utils/adb/Adb.kt | 113 ----- .../app/revanced/utils/adb/AdbManager.kt | 130 ++++++ .../kotlin/app/revanced/utils/adb/Commands.kt | 34 +- .../app/revanced/utils/adb/Constants.kt | 47 +-- .../app/revanced/utils/patcher/Patcher.kt | 75 ---- .../patcher/options/PatchOptionOptionsTest.kt | 11 +- 9 files changed, 416 insertions(+), 408 deletions(-) delete mode 100644 src/main/kotlin/app/revanced/cli/patcher/Patcher.kt delete mode 100644 src/main/kotlin/app/revanced/utils/adb/Adb.kt create mode 100644 src/main/kotlin/app/revanced/utils/adb/AdbManager.kt delete mode 100644 src/main/kotlin/app/revanced/utils/patcher/Patcher.kt diff --git a/build.gradle.kts b/build.gradle.kts index 3a0f8126..3fa04cb4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -23,9 +23,9 @@ repositories { } dependencies { + implementation("app.revanced:revanced-patcher:14.0.0") implementation("org.jetbrains.kotlin:kotlin-reflect:1.8.22") - - implementation("app.revanced:revanced-patcher:13.0.0") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1") implementation("info.picocli:picocli:4.7.3") implementation("com.github.revanced:jadb:2531a28109") // Updated fork implementation("com.android.tools.build:apksig:8.1.0") diff --git a/src/main/kotlin/app/revanced/cli/command/MainCommand.kt b/src/main/kotlin/app/revanced/cli/command/MainCommand.kt index 38d9e235..5ef13f0c 100644 --- a/src/main/kotlin/app/revanced/cli/command/MainCommand.kt +++ b/src/main/kotlin/app/revanced/cli/command/MainCommand.kt @@ -2,28 +2,26 @@ package app.revanced.cli.command import app.revanced.cli.aligning.Aligning import app.revanced.cli.logging.impl.DefaultCliLogger -import app.revanced.cli.patcher.Patcher import app.revanced.cli.patcher.logging.impl.PatcherLogger import app.revanced.cli.signing.Signing import app.revanced.cli.signing.SigningOptions +import app.revanced.patcher.PatchBundleLoader +import app.revanced.patcher.Patcher import app.revanced.patcher.PatcherOptions -import app.revanced.patcher.data.Context import app.revanced.patcher.extensions.PatchExtensions.compatiblePackages import app.revanced.patcher.extensions.PatchExtensions.description +import app.revanced.patcher.extensions.PatchExtensions.include import app.revanced.patcher.extensions.PatchExtensions.patchName -import app.revanced.patcher.patch.Patch -import app.revanced.patcher.util.patch.PatchBundle +import app.revanced.patcher.patch.PatchClass import app.revanced.utils.Options import app.revanced.utils.Options.setOptions -import app.revanced.utils.adb.Adb +import app.revanced.utils.adb.AdbManager +import kotlinx.coroutines.runBlocking import picocli.CommandLine.* import java.io.File -import java.nio.file.Files -/** - * Alias for return type of [PatchBundle.loadPatches]. - */ -internal typealias PatchList = List>> + +internal typealias PatchList = List private class CLIVersionProvider : IVersionProvider { override fun getVersion() = arrayOf( @@ -42,156 +40,192 @@ internal object MainCommand : Runnable { @ArgGroup(exclusive = false, multiplicity = "1") lateinit var args: Args + /** + * Arguments for the CLI + */ class Args { - // TODO: Move this so it is not required when listing patches - @Option(names = ["-a", "--apk"], description = ["APK file to be patched"], required = true) - lateinit var inputFile: File + @Option(names = ["--uninstall"], description = ["Package name to uninstall"]) + var packageName: String? = null - @Option(names = ["--unmount"], description = ["Unmount a patched APK file"]) - var unmount: Boolean = false + @Option(names = ["-d", "--device-serial"], description = ["ADB device serial number to deploy to"]) + var deviceSerial: String? = null - @Option( - names = ["-d", "--deploy"], - description = ["Deploy to the specified device that is connected via ADB"] - ) - var deploy: String? = null + @Option(names = ["--mount"], description = ["Handle deployments by mounting"]) + var mount: Boolean = false @ArgGroup(exclusive = false) var patchArgs: PatchArgs? = null - } - class PatchArgs { - @Option(names = ["-b", "--bundle"], description = ["One or more bundles of patches"], required = true) - var patchBundles = arrayOf() + /** + * Arguments for patches. + */ + class PatchArgs { + @Option(names = ["-b", "--bundle"], description = ["One or more bundles of patches"], required = true) + var patchBundles = emptyList() + + @ArgGroup(exclusive = false) + var listingArgs: ListingArgs? = null + + @ArgGroup(exclusive = false) + var patchingArgs: PatchingArgs? = null + + /** + * Arguments for patching. + */ + class PatchingArgs { + @Option(names = ["-a", "--apk"], description = ["APK file to be patched"], required = true) + lateinit var inputFile: File + + @Option( + names = ["-o", "--out"], + description = ["Path to save the patched APK file to"], + required = true + ) + lateinit var outputFilePath: File - @Option(names = ["--options"], description = ["Path to patch options JSON file"]) - var optionsFile: File = File("options.json") + @Option(names = ["--options"], description = ["Path to patch options JSON file"]) + var optionsFile: File = File("options.json") - @ArgGroup(exclusive = false) - var listingArgs: ListingArgs? = null + @Option(names = ["-e", "--exclude"], description = ["List of patches to exclude"]) + var excludedPatches = arrayOf() - @ArgGroup(exclusive = false) - var patchingArgs: PatchingArgs? = null - } + @Option( + names = ["--exclusive"], + description = ["Only include patches that are explicitly specified to be included"] + ) + var exclusive = false - class ListingArgs { - @Option(names = ["-l", "--list"], description = ["List patches"], required = true) - var listOnly: Boolean = false + @Option(names = ["-i", "--include"], description = ["List of patches to include"]) + var includedPatches = arrayOf() - @Option(names = ["--with-versions"], description = ["List patches with version compatibilities"]) - var withVersions: Boolean = false + @Option(names = ["--experimental"], description = ["Ignore patches incompatibility to versions"]) + var experimental: Boolean = false - @Option(names = ["--with-packages"], description = ["List patches with package compatibilities"]) - var withPackages: Boolean = false - } + @Option( + names = ["-m", "--merge"], + description = ["One or more DEX files or containers to merge into the APK"] + ) + var integrations = listOf() - class PatchingArgs { - @Option(names = ["-o", "--out"], description = ["Path to save the patched APK file to"], required = true) - lateinit var outputPath: String + @Option(names = ["--cn"], description = ["The common name of the signer of the patched APK file"]) + var commonName = "ReVanced" - @Option(names = ["-e", "--exclude"], description = ["Exclude patches"]) - var excludedPatches = arrayOf() + @Option( + names = ["--keystore"], + description = ["Path to the keystore to sign the patched APK file with"] + ) + var keystorePath: String? = null - @Option( - names = ["--exclusive"], - description = ["Only include patches that were explicitly specified to be included"] - ) - var exclusive = false + @Option( + names = ["-p", "--password"], + description = ["The password of the keystore to sign the patched APK file with"] + ) + var password = "ReVanced" - @Option(names = ["-i", "--include"], description = ["Include patches"]) - var includedPatches = arrayOf() + @Option( + names = ["-r", "--resource-cache"], + description = ["Path to temporary resource cache directory"] + ) + var resourceCachePath = File("revanced-resource-cache") - @Option(names = ["--experimental"], description = ["Ignore patches incompatibility to versions"]) - var experimental: Boolean = false + @Option( + names = ["-c", "--clean"], + description = ["Clean up the temporary resource cache directory after patching"] + ) + var clean: Boolean = false - @Option(names = ["-m", "--merge"], description = ["One or more DEX files or containers to merge into the APK"]) - var mergeFiles = listOf() + @Option( + names = ["--custom-aapt2-binary"], + description = ["Path to a custom AAPT binary to compile resources with"] + ) + var aaptBinaryPath = File("") + } - @Option( - names = ["--mount"], - description = ["Mount the patched APK file over the original file instead of installing it"] - ) - var mount: Boolean = false + /** + * Arguments for printing patches to the console. + */ + class ListingArgs { + @Option(names = ["-l", "--list"], description = ["List patches"], required = true) + var listOnly: Boolean = false - @Option(names = ["--cn"], description = ["The common name of the signer of the patched APK file"]) - var cn = "ReVanced" + @Option(names = ["--with-versions"], description = ["List patches and their compatible versions"]) + var withVersions: Boolean = false - @Option(names = ["--keystore"], description = ["Path to the keystore to sign the patched APK file with"]) - var keystorePath: String? = null + @Option(names = ["--with-packages"], description = ["List patches and their compatible packages"]) + var withPackages: Boolean = false + } + } + } - @Option( - names = ["-p", "--password"], - description = ["The password of the keystore to sign the patched APK file with"] - ) - var password = "ReVanced" + override fun run() { + val patchArgs = args.patchArgs - @Option(names = ["-t", "--temp-dir"], description = ["Path to temporary resource cache directory"]) - var cacheDirectory = "revanced-cache" + if (patchArgs?.listingArgs?.listOnly == true) return printListOfPatches() + if (args.packageName != null) return uninstall() - @Option( - names = ["-c", "--clean"], - description = ["Clean up the temporary resource cache directory after patching"] - ) - var clean: Boolean = false + val patchingArgs = patchArgs?.patchingArgs ?: return - @Option( - names = ["--custom-aapt2-binary"], - description = ["Path to custom AAPT binary to compile resources with"] - ) - var aaptPath: String = "" - } + if (!patchingArgs.inputFile.exists()) return logger.error("Input file ${patchingArgs.inputFile} does not exist.") - override fun run() { - if (args.patchArgs?.listingArgs?.listOnly == true) return printListOfPatches() - if (args.unmount) return unmount() + logger.info("Loading patches") - val pArgs = this.args.patchArgs?.patchingArgs ?: return - val outputFile = File(pArgs.outputPath) // the file to write to + val patches = PatchBundleLoader.Jar(*patchArgs.patchBundles.toTypedArray()) + val integrations = patchingArgs.integrations - val allPatches = args.patchArgs!!.patchBundles.flatMap { bundle -> - PatchBundle.Jar(bundle).loadPatches() + logger.info("Setting up patch options") + + patchingArgs.optionsFile.let { + if (it.exists()) patches.setOptions(it, logger) + else Options.serialize(patches, prettyPrint = true).let(it::writeText) } - args.patchArgs!!.optionsFile.let { - if (it.exists()) allPatches.setOptions(it, logger) - else Options.serialize(allPatches, prettyPrint = true).let(it::writeText) + val adbManager = args.deviceSerial?.let { serial -> + if (args.mount) AdbManager.RootAdbManager(serial, logger) else AdbManager.UserAdbManager(serial, logger) } - val patcher = app.revanced.patcher.Patcher( + val patcher = Patcher( PatcherOptions( - args.inputFile.also { if (!it.exists()) return logger.error("Input file ${args.inputFile} does not exist.") }, - pArgs.cacheDirectory, - pArgs.aaptPath, - pArgs.cacheDirectory, + patchingArgs.inputFile, + patchingArgs.resourceCachePath, + patchingArgs.aaptBinaryPath.absolutePath, + patchingArgs.resourceCachePath.absolutePath, PatcherLogger ) ) - // prepare adb - val adb: Adb? = args.deploy?.let { - Adb(outputFile, patcher.context.packageMetadata.packageName, args.deploy!!, !pArgs.mount) - } + val result = patcher.apply { + acceptIntegrations(integrations) + acceptPatches(filterPatchSelection(patches)) + + // Execute patches. + runBlocking { + apply(false).collect { patchResult -> + patchResult.exception?.let { + logger.error("${patchResult.patchName} failed:\n${patchResult.exception}") + } ?: logger.info("${patchResult.patchName} succeeded") + } + } + }.get() - // start the patcher - val result = Patcher.start(patcher, allPatches) + patcher.close() - val cacheDirectory = File(pArgs.cacheDirectory) + val outputFileNameWithoutExtension = patchingArgs.outputFilePath.nameWithoutExtension - // align the file - val alignedFile = cacheDirectory.resolve("${outputFile.nameWithoutExtension}_aligned.apk") - Aligning.align(result, args.inputFile, alignedFile) + // Align the file. + val alignedFile = patchingArgs.resourceCachePath.resolve("${outputFileNameWithoutExtension}_aligned.apk") + Aligning.align(result, patchingArgs.inputFile, alignedFile) - // sign the file - val finalFile = if (!pArgs.mount) { - val signedOutput = cacheDirectory.resolve("${outputFile.nameWithoutExtension}_signed.apk") + // Sign the file if needed. + val finalFile = if (!args.mount) { + val signedOutput = patchingArgs.resourceCachePath.resolve("${outputFileNameWithoutExtension}_signed.apk") Signing.sign( alignedFile, signedOutput, SigningOptions( - pArgs.cn, - pArgs.password, - pArgs.keystorePath ?: outputFile.absoluteFile.parentFile - .resolve("${outputFile.nameWithoutExtension}.keystore") + patchingArgs.commonName, + patchingArgs.password, + patchingArgs.keystorePath ?: patchingArgs.outputFilePath.absoluteFile.parentFile + .resolve("${patchingArgs.outputFilePath.nameWithoutExtension}.keystore") .canonicalPath ) ) @@ -200,46 +234,41 @@ internal object MainCommand : Runnable { } else alignedFile - // finally copy to the specified output file - logger.info("Copying ${finalFile.name} to ${outputFile.name}") - finalFile.copyTo(outputFile, overwrite = true) + logger.info("Copying ${finalFile.name} to ${patchingArgs.outputFilePath.name}") - // clean up the cache directory if needed - if (pArgs.clean) - cleanUp(pArgs.cacheDirectory) + finalFile.copyTo(patchingArgs.outputFilePath, overwrite = true) + adbManager?.install(AdbManager.Apk(patchingArgs.outputFilePath, patcher.context.packageMetadata.packageName)) - // deploy if specified - adb?.deploy() - - if (pArgs.clean && args.deploy != null) Files.delete(outputFile.toPath()) - - logger.info("Finished") + if (patchingArgs.clean) { + logger.info("Cleaning up temporary files") + patchingArgs.outputFilePath.delete() + cleanUp(patchingArgs.resourceCachePath) + } } - private fun cleanUp(cacheDirectory: String) { - val result = if (File(cacheDirectory).deleteRecursively()) + private fun cleanUp(resourceCachePath: File) { + val result = if (resourceCachePath.deleteRecursively()) "Cleaned up cache directory" else "Failed to clean up cache directory" logger.info(result) } - private fun unmount() { - val adb: Adb? = args.deploy?.let { - Adb( - File("placeholder_file"), - app.revanced.patcher.Patcher(PatcherOptions(args.inputFile, "")).context.packageMetadata.packageName, - args.deploy!!, - false - ) - } - adb?.uninstall() - } + /** + * Uninstall the specified package from the specified device. + * + */ + private fun uninstall() = args.deviceSerial?.let { serial -> + if (args.mount) { + AdbManager.RootAdbManager(serial, logger) + } else { + AdbManager.UserAdbManager(serial, logger) + }.uninstall(args.packageName!!) + } ?: logger.error("No device serial specified") private fun printListOfPatches() { val logged = mutableListOf() - for (patchBundlePath in args.patchArgs?.patchBundles!!) for (patch in PatchBundle.Jar(patchBundlePath) - .loadPatches()) { + for (patch in PatchBundleLoader.Jar(*args.patchArgs!!.patchBundles.toTypedArray())) { if (patch.patchName in logged) continue for (compatiblePackage in patch.compatiblePackages ?: continue) { val packageEntryStr = buildString { @@ -271,4 +300,78 @@ internal object MainCommand : Runnable { } } } -} + + private fun Patcher.filterPatchSelection(patches: PatchList) = buildList { + val packageName = context.packageMetadata.packageName + val packageVersion = context.packageMetadata.packageVersion + val patchingArgs = args.patchArgs!!.patchingArgs!! + + patches.forEach patch@{ patch -> + val formattedPatchName = patch.patchName.lowercase().replace(" ", "-") + + /** + * Check if the patch is explicitly excluded. + * + * Cases: + * 1. -e patch.name + * 2. -i patch.name -e patch.name + */ + + val excluded = patchingArgs.excludedPatches.contains(formattedPatchName) + if (excluded) return@patch logger.info("Excluding ${patch.patchName}") + + /** + * Check if the patch is constrained to packages. + */ + + patch.compatiblePackages?.let { packages -> + packages.singleOrNull { it.name == packageName }?.let { `package` -> + /** + * Check if the package version matches. + * If experimental is true, version matching will be skipped. + */ + + val matchesVersion = patchingArgs.experimental || `package`.versions.let { + it.isEmpty() || it.any { version -> version == packageVersion } + } + + if (!matchesVersion) return@patch logger.warn( + "${patch.patchName} is incompatible with version $packageVersion. " + + "This patch is only compatible with version " + + packages.joinToString(";") { `package` -> + "${`package`.name}: ${`package`.versions.joinToString(", ")}" + } + ) + + } ?: return@patch logger.trace( + "${patch.patchName} is incompatible with $packageName. " + + "This patch is only compatible with " + + packages.joinToString(", ") { `package` -> `package`.name } + ) + + return@let + } ?: logger.trace("$formattedPatchName: No constraint on packages.") + + /** + * Check if the patch is explicitly included. + * + * Cases: + * 1. --exclusive + * 2. --exclusive -i patch.name + */ + + val exclusive = patchingArgs.exclusive + val explicitlyIncluded = patchingArgs.includedPatches.contains(formattedPatchName) + + val implicitlyIncluded = !exclusive && patch.include // Case 3. + val exclusivelyIncluded = exclusive && explicitlyIncluded // Case 2. + + val included = implicitlyIncluded || exclusivelyIncluded + if (!included) return@patch logger.info("${patch.patchName} excluded by default") // Case 1. + + logger.trace("Adding $formattedPatchName") + + add(patch) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/cli/patcher/Patcher.kt b/src/main/kotlin/app/revanced/cli/patcher/Patcher.kt deleted file mode 100644 index 180703e2..00000000 --- a/src/main/kotlin/app/revanced/cli/patcher/Patcher.kt +++ /dev/null @@ -1,23 +0,0 @@ -package app.revanced.cli.patcher - -import app.revanced.cli.command.PatchList -import app.revanced.patcher.PatcherResult -import app.revanced.utils.patcher.addPatchesFiltered -import app.revanced.utils.patcher.applyPatchesVerbose -import app.revanced.utils.patcher.mergeFiles - -internal object Patcher { - internal fun start( - patcher: app.revanced.patcher.Patcher, - allPatches: PatchList - ): PatcherResult { - // merge files like necessary integrations - patcher.mergeFiles() - // add patches, but filter incompatible or excluded patches - patcher.addPatchesFiltered(allPatches) - // apply patches - patcher.applyPatchesVerbose() - - return patcher.save() - } -} diff --git a/src/main/kotlin/app/revanced/utils/adb/Adb.kt b/src/main/kotlin/app/revanced/utils/adb/Adb.kt deleted file mode 100644 index 3c515660..00000000 --- a/src/main/kotlin/app/revanced/utils/adb/Adb.kt +++ /dev/null @@ -1,113 +0,0 @@ -package app.revanced.utils.adb - -import app.revanced.cli.command.MainCommand.logger -import se.vidstige.jadb.JadbConnection -import se.vidstige.jadb.JadbDevice -import se.vidstige.jadb.managers.PackageManager -import java.io.File -import java.util.concurrent.Executors - -internal class Adb( - private val file: File, - private val packageName: String, - deviceName: String, - private val install: Boolean = false, - private val logging: Boolean = true -) { - private val device: JadbDevice - - init { - device = JadbConnection().devices.let { device -> device.find { it.serial == deviceName } ?: device.first() } - ?: throw IllegalArgumentException("No such device with name $deviceName") - - if (!install && device.run("su -h", false) != 0) - throw IllegalArgumentException("Root required on $deviceName. Task failed") - } - - private fun String.replacePlaceholder(with: String? = null): String { - return this.replace(Constants.PLACEHOLDER, with ?: packageName) - } - - internal fun deploy() { - if (install) { - logger.info("Installing without mounting") - - PackageManager(device).install(file) - } else { - logger.info("Installing by mounting") - - // push patched file - device.copy(Constants.PATH_INIT_PUSH, file) - - // create revanced folder path - device.run("${Constants.COMMAND_CREATE_DIR} ${Constants.PATH_REVANCED}") - - // prepare mounting the apk - device.run(Constants.COMMAND_PREPARE_MOUNT_APK.replacePlaceholder()) - - // push mount script - device.createFile( - Constants.PATH_INIT_PUSH, - Constants.CONTENT_MOUNT_SCRIPT.replacePlaceholder() - ) - // install mount script - device.run(Constants.COMMAND_INSTALL_MOUNT.replacePlaceholder()) - - // unmount the apk for sanity - device.run(Constants.COMMAND_UMOUNT.replacePlaceholder()) - // mount the apk - device.run(Constants.PATH_MOUNT.replacePlaceholder()) - - // relaunch app - device.run(Constants.COMMAND_RESTART.replacePlaceholder()) - - // log the app - log() - } - } - - internal fun uninstall() { - logger.info("Uninstalling by unmounting") - - // unmount the apk - device.run(Constants.COMMAND_UMOUNT.replacePlaceholder()) - - // delete revanced app - device.run(Constants.COMMAND_DELETE.replacePlaceholder(Constants.PATH_REVANCED_APP).replacePlaceholder()) - - // delete mount script - device.run(Constants.COMMAND_DELETE.replacePlaceholder(Constants.PATH_MOUNT).replacePlaceholder()) - - logger.info("Finished uninstalling") - } - - private fun log() { - val executor = Executors.newSingleThreadExecutor() - val pipe = if (logging) { - ProcessBuilder.Redirect.INHERIT - } else { - ProcessBuilder.Redirect.PIPE - } - - val process = device.buildCommand(Constants.COMMAND_LOGCAT.replacePlaceholder()) - .redirectOutput(pipe) - .redirectError(pipe) - .useExecutor(executor) - .start() - - Thread.sleep(500) // give the app some time to start up. - while (true) { - try { - while (device.run("${Constants.COMMAND_PID_OF} $packageName") == 0) { - Thread.sleep(1000) - } - break - } catch (e: Exception) { - throw RuntimeException("An error occurred while monitoring the state of app", e) - } - } - logger.info("Stopped logging because the app was closed") - process.destroy() - executor.shutdown() - } -} diff --git a/src/main/kotlin/app/revanced/utils/adb/AdbManager.kt b/src/main/kotlin/app/revanced/utils/adb/AdbManager.kt new file mode 100644 index 00000000..1bbc28d5 --- /dev/null +++ b/src/main/kotlin/app/revanced/utils/adb/AdbManager.kt @@ -0,0 +1,130 @@ +package app.revanced.utils.adb + +import app.revanced.cli.logging.CliLogger +import app.revanced.utils.adb.AdbManager.Apk +import app.revanced.utils.adb.Constants.COMMAND_CREATE_DIR +import app.revanced.utils.adb.Constants.COMMAND_DELETE +import app.revanced.utils.adb.Constants.COMMAND_INSTALL_MOUNT +import app.revanced.utils.adb.Constants.COMMAND_PREPARE_MOUNT_APK +import app.revanced.utils.adb.Constants.COMMAND_RESTART +import app.revanced.utils.adb.Constants.COMMAND_UMOUNT +import app.revanced.utils.adb.Constants.CONTENT_MOUNT_SCRIPT +import app.revanced.utils.adb.Constants.PATH_INIT_PUSH +import app.revanced.utils.adb.Constants.PATH_INSTALLATION +import app.revanced.utils.adb.Constants.PATH_MOUNT +import app.revanced.utils.adb.Constants.PATH_PATCHED_APK +import app.revanced.utils.adb.Constants.PLACEHOLDER +import se.vidstige.jadb.JadbConnection +import se.vidstige.jadb.managers.Package +import se.vidstige.jadb.managers.PackageManager +import java.io.Closeable +import java.io.File + +/** + * Adb manager. Used to install and uninstall [Apk] files. + * + * @param deviceSerial The serial of the device. + */ +internal sealed class AdbManager(deviceSerial: String? = null, protected val logger: CliLogger? = null) : Closeable { + protected val device = JadbConnection().devices.find { device -> device.serial == deviceSerial } + ?: throw IllegalArgumentException("The device with the serial $deviceSerial can not be found.") + + init { + logger?.trace("Established connection to $deviceSerial") + } + + /** + * Installs the [Apk] file. + * + * @param apk The [Apk] file. + */ + open fun install(apk: Apk) { + logger?.info("Finished installing ${apk.file.name}") + } + + /** + * Uninstalls the package. + * + * @param packageName The package name. + */ + open fun uninstall(packageName: String) { + logger?.info("Finished uninstalling $packageName") + } + + /** + * Closes the [AdbManager] instance. + */ + override fun close() { + logger?.trace("Closed") + } + + class RootAdbManager(deviceSerial: String, logger: CliLogger? = null) : AdbManager(deviceSerial, logger) { + init { + if (!device.hasSu()) throw IllegalArgumentException("Root required on $deviceSerial. Task failed") + } + + override fun install(apk: Apk) { + logger?.info("Installing by mounting") + + val applyReplacement = getPlaceholderReplacement( + apk.packageName ?: throw IllegalArgumentException("Package name is required") + ) + + device.copyFile(apk.file, PATH_INIT_PUSH) + + device.run("$COMMAND_CREATE_DIR $PATH_INSTALLATION") + device.run(COMMAND_PREPARE_MOUNT_APK.applyReplacement()) + + device.createFile(PATH_INIT_PUSH, CONTENT_MOUNT_SCRIPT.applyReplacement()) + + device.run(COMMAND_INSTALL_MOUNT.applyReplacement()) + device.run(COMMAND_UMOUNT.applyReplacement()) // Sanity check. + device.run(PATH_MOUNT.applyReplacement()) + device.run(COMMAND_RESTART.applyReplacement()) + + super.install(apk) + } + + override fun uninstall(packageName: String) { + logger?.info("Uninstalling $packageName by unmounting and deleting the package") + + val applyReplacement = getPlaceholderReplacement(packageName) + + device.run(COMMAND_UMOUNT.applyReplacement(packageName)) + device.run(COMMAND_DELETE.applyReplacement(PATH_PATCHED_APK).applyReplacement()) + device.run(COMMAND_DELETE.applyReplacement(PATH_MOUNT).applyReplacement()) + + super.uninstall(packageName) + } + + companion object Utils { + private fun getPlaceholderReplacement(with: String): String.() -> String = { replace(PLACEHOLDER, with) } + private fun String.applyReplacement(with: String) = replace(PLACEHOLDER, with) + } + } + + class UserAdbManager(deviceSerial: String, logger: CliLogger? = null) : AdbManager(deviceSerial, logger) { + private val packageManager = PackageManager(device) + + override fun install(apk: Apk) { + PackageManager(device).install(apk.file) + + super.install(apk) + } + + override fun uninstall(packageName: String) { + logger?.info("Uninstalling $packageName") + + packageManager.uninstall(Package(packageName)) + + super.uninstall(packageName) + } + } + + /** + * Apk file for [AdbManager]. + * + * @param file The [Apk] file. + */ + internal class Apk(val file: File, val packageName: String? = null) +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/utils/adb/Commands.kt b/src/main/kotlin/app/revanced/utils/adb/Commands.kt index 1b3af07f..744ebdaa 100644 --- a/src/main/kotlin/app/revanced/utils/adb/Commands.kt +++ b/src/main/kotlin/app/revanced/utils/adb/Commands.kt @@ -2,28 +2,28 @@ package app.revanced.utils.adb import se.vidstige.jadb.JadbDevice import se.vidstige.jadb.RemoteFile -import se.vidstige.jadb.ShellProcessBuilder import java.io.File +import java.util.concurrent.Callable +import java.util.concurrent.Executors -internal fun JadbDevice.buildCommand(command: String, su: Boolean = true): ShellProcessBuilder { - if (su) { - return shellProcessBuilder("su -c \'$command\'") +// return the input or output stream, depending on which first returns a value +internal fun JadbDevice.run(command: String, su: Boolean = false) = with(this.startCommand(command, su)) { + Executors.newFixedThreadPool(2).let { service -> + arrayOf(inputStream, errorStream).map { stream -> + Callable { stream.bufferedReader().use { it.readLine() } } + }.let { tasks -> service.invokeAny(tasks).also { service.shutdown() } } } - - val args = command.split(" ") as ArrayList - val cmd = args.removeFirst() - - return shellProcessBuilder(cmd, *args.toTypedArray()) } -internal fun JadbDevice.run(command: String, su: Boolean = true): Int { - return this.buildCommand(command, su).start().waitFor() -} +internal fun JadbDevice.hasSu() = + this.startCommand("su -h", false).waitFor() == 0 -internal fun JadbDevice.copy(targetPath: String, file: File) { - push(file, RemoteFile(targetPath)) -} +internal fun JadbDevice.copyFile(file: File, targetFile: String) = + push(file, RemoteFile(targetFile)) -internal fun JadbDevice.createFile(targetFile: String, content: String) { +internal fun JadbDevice.createFile(targetFile: String, content: String) = push(content.byteInputStream(), System.currentTimeMillis(), 644, RemoteFile(targetFile)) -} \ No newline at end of file + + +private fun JadbDevice.startCommand(command: String, su: Boolean) = + shellProcessBuilder(if (su) "su -c '$command'" else command).start() \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/utils/adb/Constants.kt b/src/main/kotlin/app/revanced/utils/adb/Constants.kt index f4aa5f22..0d97211d 100644 --- a/src/main/kotlin/app/revanced/utils/adb/Constants.kt +++ b/src/main/kotlin/app/revanced/utils/adb/Constants.kt @@ -1,57 +1,40 @@ package app.revanced.utils.adb internal object Constants { - // template placeholder to replace a string in commands internal const val PLACEHOLDER = "TEMPLATE_PACKAGE_NAME" - // utility commands - private const val COMMAND_CHMOD_MOUNT = "chmod +x" - internal const val COMMAND_PID_OF = "pidof -s" - internal const val COMMAND_CREATE_DIR = "mkdir -p" - internal const val COMMAND_LOGCAT = "logcat -c && logcat | grep AndroidRuntime" - internal const val COMMAND_RESTART = "pm resolve-activity --brief $PLACEHOLDER | tail -n 1 | xargs am start -n && kill ${'$'}($COMMAND_PID_OF $PLACEHOLDER)" - - // default mount file name - private const val NAME_MOUNT_SCRIPT = "mount_revanced_$PLACEHOLDER.sh" - - // initial directory to push files to via adb push internal const val PATH_INIT_PUSH = "/data/local/tmp/revanced.delete" + internal const val PATH_INSTALLATION = "/data/adb/revanced/" + internal const val PATH_PATCHED_APK = "$PATH_INSTALLATION$PLACEHOLDER.apk" + internal const val PATH_MOUNT = "/data/adb/service.d/mount_revanced_$PLACEHOLDER.sh" - // revanced path - internal const val PATH_REVANCED = "/data/adb/revanced/" - - // revanced apk path - internal const val PATH_REVANCED_APP = "$PATH_REVANCED$PLACEHOLDER.apk" - - // delete command internal const val COMMAND_DELETE = "rm -rf $PLACEHOLDER" + internal const val COMMAND_CREATE_DIR = "mkdir -p" + internal const val COMMAND_RESTART = "pm resolve-activity --brief $PLACEHOLDER | tail -n 1 | " + + "xargs am start -n && kill ${'$'}(pidof -s $PLACEHOLDER)" - // mount script path - internal const val PATH_MOUNT = "/data/adb/service.d/$NAME_MOUNT_SCRIPT" - - // move to revanced apk path & set permissions - internal const val COMMAND_PREPARE_MOUNT_APK = - "base_path=\"$PATH_REVANCED_APP\" && mv $PATH_INIT_PUSH ${'$'}base_path && chmod 644 ${'$'}base_path && chown system:system ${'$'}base_path && chcon u:object_r:apk_data_file:s0 ${'$'}base_path" + internal const val COMMAND_PREPARE_MOUNT_APK = "base_path=\"$PATH_PATCHED_APK\" && " + + "mv $PATH_INIT_PUSH ${'$'}base_path && " + + "chmod 644 ${'$'}base_path && " + + "chown system:system ${'$'}base_path && " + + "chcon u:object_r:apk_data_file:s0 ${'$'}base_path" - // unmount command internal const val COMMAND_UMOUNT = "grep $PLACEHOLDER /proc/mounts | while read -r line; do echo ${'$'}line | cut -d \" \" -f 2 | sed 's/apk.*/apk/' | xargs -r umount -l; done" - // install mount script & set permissions - internal const val COMMAND_INSTALL_MOUNT = "mv $PATH_INIT_PUSH $PATH_MOUNT && $COMMAND_CHMOD_MOUNT $PATH_MOUNT" + internal const val COMMAND_INSTALL_MOUNT = "mv $PATH_INIT_PUSH $PATH_MOUNT && chmod +x $PATH_MOUNT" - // mount script - internal val CONTENT_MOUNT_SCRIPT = + internal const val CONTENT_MOUNT_SCRIPT = """ #!/system/bin/sh MAGISKTMP="${'$'}(magisk --path)" || MAGISKTMP=/sbin MIRROR="${'$'}MAGISKTMP/.magisk/mirror" while [ "${'$'}(getprop sys.boot_completed | tr -d '\r')" != "1" ]; do sleep 1; done - base_path="$PATH_REVANCED_APP" + base_path="$PATH_PATCHED_APK" stock_path=${'$'}( pm path $PLACEHOLDER | grep base | sed 's/package://g' ) chcon u:object_r:apk_data_file:s0 ${'$'}base_path mount -o bind ${'$'}MIRROR${'$'}base_path ${'$'}stock_path - """.trimIndent() + """ } diff --git a/src/main/kotlin/app/revanced/utils/patcher/Patcher.kt b/src/main/kotlin/app/revanced/utils/patcher/Patcher.kt deleted file mode 100644 index fd61bae0..00000000 --- a/src/main/kotlin/app/revanced/utils/patcher/Patcher.kt +++ /dev/null @@ -1,75 +0,0 @@ -package app.revanced.utils.patcher - -import app.revanced.cli.command.MainCommand.args -import app.revanced.cli.command.MainCommand.logger -import app.revanced.cli.command.PatchList -import app.revanced.patcher.Patcher -import app.revanced.patcher.data.Context -import app.revanced.patcher.extensions.PatchExtensions.compatiblePackages -import app.revanced.patcher.extensions.PatchExtensions.include -import app.revanced.patcher.extensions.PatchExtensions.patchName -import app.revanced.patcher.patch.Patch - -fun Patcher.addPatchesFiltered(allPatches: PatchList) { - val packageName = this.context.packageMetadata.packageName - val packageVersion = this.context.packageMetadata.packageVersion - - val includedPatches = mutableListOf>>() - allPatches.forEach patchLoop@{ patch -> - val compatiblePackages = patch.compatiblePackages - val args = args.patchArgs?.patchingArgs!! - - val prefix = "Skipping ${patch.patchName}" - - if (compatiblePackages == null) logger.trace("${patch.patchName}: No package constraints.") - else { - if (!compatiblePackages.any { it.name == packageName }) { - logger.trace("$prefix: Incompatible with $packageName. This patch is only compatible with ${ - compatiblePackages.joinToString( - ", " - ) { it.name } - }") - return@patchLoop - } - - if (!(args.experimental || compatiblePackages.any { it.versions.isEmpty() || it.versions.any { version -> version == packageVersion } })) { - val compatibleWith = compatiblePackages.joinToString(";") { _package -> - "${_package.name}: ${_package.versions.joinToString(", ")}" - } - logger.warn("$prefix: Incompatible with version $packageVersion. This patch is only compatible with $compatibleWith") - return@patchLoop - } - } - - val kebabCasedPatchName = patch.patchName.lowercase().replace(" ", "-") - if (args.excludedPatches.contains(kebabCasedPatchName)) { - logger.info("$prefix: Manually excluded") - return@patchLoop - } else if ((!patch.include || args.exclusive) && !args.includedPatches.contains(kebabCasedPatchName)) { - logger.info("$prefix: Excluded by default") - return@patchLoop - } - - logger.trace("Adding ${patch.patchName}") - includedPatches.add(patch) - } - - this.addPatches(includedPatches) -} - -fun Patcher.applyPatchesVerbose() { - this.executePatches().forEach { (patch, result) -> - if (result.isSuccess) { - logger.info("$patch succeeded") - return@forEach - } - logger.error("$patch failed:") - result.exceptionOrNull()!!.printStackTrace() - } -} - -fun Patcher.mergeFiles() { - this.addIntegrations(args.patchArgs?.patchingArgs!!.mergeFiles) { file -> - logger.info("Merging $file") - } -} diff --git a/src/test/kotlin/app/revanced/patcher/options/PatchOptionOptionsTest.kt b/src/test/kotlin/app/revanced/patcher/options/PatchOptionOptionsTest.kt index 68759eec..9abfca55 100644 --- a/src/test/kotlin/app/revanced/patcher/options/PatchOptionOptionsTest.kt +++ b/src/test/kotlin/app/revanced/patcher/options/PatchOptionOptionsTest.kt @@ -2,7 +2,10 @@ package app.revanced.patcher.options import app.revanced.patcher.data.BytecodeContext import app.revanced.patcher.data.Context -import app.revanced.patcher.patch.* +import app.revanced.patcher.patch.BytecodePatch +import app.revanced.patcher.patch.OptionsContainer +import app.revanced.patcher.patch.Patch +import app.revanced.patcher.patch.PatchOption import app.revanced.utils.Options import app.revanced.utils.Options.setOptions import org.junit.jupiter.api.MethodOrderer @@ -11,8 +14,8 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestMethodOrder class PatchOptionsTestPatch : BytecodePatch() { - override fun execute(context: BytecodeContext): PatchResult { - return PatchResultSuccess() + override fun execute(context: BytecodeContext) { + // Do nothing } companion object : OptionsContainer() { @@ -32,7 +35,7 @@ class PatchOptionsTestPatch : BytecodePatch() { @TestMethodOrder(MethodOrderer.OrderAnnotation::class) internal object PatchOptionOptionsTest { - private var patches = listOf(PatchOptionsTestPatch::class.java as Class>) + private var patches = listOf(PatchOptionsTestPatch::class.java as Class>>) @Test @Order(1)