Compare commits

...

24 Commits

Author SHA1 Message Date
semantic-release-bot
9e39a6f8e4 chore(release): 3.0.0-dev.10 [skip ci]
# [3.0.0-dev.10](https://github.com/ReVanced/revanced-cli/compare/v3.0.0-dev.9...v3.0.0-dev.10) (2023-08-25)

### Bug Fixes

* filtration of patches malfunctioning ([2d5a7fd](2d5a7fdf1e))
2023-08-25 21:48:59 +00:00
oSumAtrIX
2d5a7fdf1e fix: filtration of patches malfunctioning
Apparently, you were not able to include patches explicitly
2023-08-25 23:47:18 +02:00
semantic-release-bot
be5d812dff chore(release): 3.0.0-dev.9 [skip ci]
# [3.0.0-dev.9](https://github.com/ReVanced/revanced-cli/compare/v3.0.0-dev.8...v3.0.0-dev.9) (2023-08-25)

### Features

* Check for missing integrations ([c93186f](c93186fb97))
2023-08-25 00:28:57 +00:00
oSumAtrIX
c93186fb97 feat: Check for missing integrations
Check, if the integrations file exists at first.
2023-08-25 02:26:38 +02:00
semantic-release-bot
3a198052bb chore(release): 3.0.0-dev.8 [skip ci]
# [3.0.0-dev.8](https://github.com/ReVanced/revanced-cli/compare/v3.0.0-dev.7...v3.0.0-dev.8) (2023-08-24)

### Bug Fixes

* do not delete output file ([0f3e090](0f3e090418))
2023-08-24 23:32:29 +00:00
oSumAtrIX
0f3e090418 fix: do not delete output file
This fixes the output file to be deleted when the option `--purge` was used.
2023-08-25 01:30:28 +02:00
semantic-release-bot
6aed946183 chore(release): 3.0.0-dev.7 [skip ci]
# [3.0.0-dev.7](https://github.com/ReVanced/revanced-cli/compare/v3.0.0-dev.6...v3.0.0-dev.7) (2023-08-24)

### Bug Fixes

* print stack trace when a patch failed ([924c1f8](924c1f80ec))
2023-08-24 21:47:17 +00:00
oSumAtrIX
924c1f80ec fix: print stack trace when a patch failed 2023-08-24 23:45:10 +02:00
semantic-release-bot
8dd709b6ef chore(release): 3.0.0-dev.6 [skip ci]
# [3.0.0-dev.6](https://github.com/ReVanced/revanced-cli/compare/v3.0.0-dev.5...v3.0.0-dev.6) (2023-08-24)
2023-08-24 16:11:51 +00:00
oSumAtrIX
139e7facac build(Needs bump): depend on build task when publishing
This fixes the issue that no builds are generated
2023-08-24 18:09:35 +02:00
semantic-release-bot
1d26e572f7 chore(release): 3.0.0-dev.5 [skip ci]
# [3.0.0-dev.5](https://github.com/ReVanced/revanced-cli/compare/v3.0.0-dev.4...v3.0.0-dev.5) (2023-08-24)

### Bug Fixes

* also delete temporary files when uninstalling ([52c3be2](52c3be23f2))
* delete temporary files after root installation ([a3d8705](a3d8705e89))
* fix running commands not running ([2c7fcaf](2c7fcaf4ad))
* only check once for patch options ([11c3a6c](11c3a6cfd4))

### Features

* add install command ([0350b7f](0350b7f1a2))
* use friendly descriptions ([3dd875d](3dd875d14c))
2023-08-24 15:53:09 +00:00
oSumAtrIX
2c7fcaf4ad fix: fix running commands not running 2023-08-24 17:51:31 +02:00
oSumAtrIX
52c3be23f2 fix: also delete temporary files when uninstalling 2023-08-24 16:54:08 +02:00
oSumAtrIX
3dd875d14c feat: use friendly descriptions 2023-08-24 16:53:31 +02:00
oSumAtrIX
0350b7f1a2 feat: add install command
This introduces a separate utility subcommand.
2023-08-24 16:50:10 +02:00
oSumAtrIX
a3d8705e89 fix: delete temporary files after root installation 2023-08-24 16:24:33 +02:00
oSumAtrIX
a536c9f815 refactor: use better identifiers 2023-08-24 16:24:18 +02:00
oSumAtrIX
11c3a6cfd4 fix: only check once for patch options
This prevents checking for the same patches options multiple times when it is already determined to not have any options
2023-08-24 15:52:12 +02:00
oSumAtrIX
a5851f0c1a build: clean after building 2023-08-24 15:35:27 +02:00
oSumAtrIX
963ae3a5fa docs: add missing inline docs 2023-08-24 15:35:27 +02:00
semantic-release-bot
93f338a731 chore(release): 3.0.0-dev.4 [skip ci]
# [3.0.0-dev.4](https://github.com/ReVanced/revanced-cli/compare/v3.0.0-dev.3...v3.0.0-dev.4) (2023-08-24)

### Features

* properly make use of logging facade ([41898d7](41898d7547))
2023-08-24 02:56:08 +00:00
oSumAtrIX
7dcf15e03b build: bump dependencies 2023-08-24 04:52:53 +02:00
oSumAtrIX
41898d7547 feat: properly make use of logging facade 2023-08-24 04:41:44 +02:00
oSumAtrIX
45dd15f679 build: do not use a daemon when building
This decreases build times
2023-08-23 16:43:57 +02:00
23 changed files with 400 additions and 383 deletions

View File

@@ -33,10 +33,11 @@ jobs:
build build
node_modules node_modules
key: ${{ runner.os }}-gradle-npm-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties', 'package-lock.json') }} key: ${{ runner.os }}-gradle-npm-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties', 'package-lock.json') }}
- name: Build with Gradle - name: Build
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: ./gradlew build # Cleaning is necessary to avoid uploading two identical artifacts with different versions
run: ./gradlew clean --no-daemon
- name: Setup semantic-release - name: Setup semantic-release
run: npm install run: npm install
- name: Release - name: Release

View File

@@ -1,3 +1,56 @@
# [3.0.0-dev.10](https://github.com/ReVanced/revanced-cli/compare/v3.0.0-dev.9...v3.0.0-dev.10) (2023-08-25)
### Bug Fixes
* filtration of patches malfunctioning ([2d5a7fd](https://github.com/ReVanced/revanced-cli/commit/2d5a7fdf1eb2e13f5013a790b03f09851b167fe0))
# [3.0.0-dev.9](https://github.com/ReVanced/revanced-cli/compare/v3.0.0-dev.8...v3.0.0-dev.9) (2023-08-25)
### Features
* Check for missing integrations ([c93186f](https://github.com/ReVanced/revanced-cli/commit/c93186fb9700907e65f33442e88073783cc163de))
# [3.0.0-dev.8](https://github.com/ReVanced/revanced-cli/compare/v3.0.0-dev.7...v3.0.0-dev.8) (2023-08-24)
### Bug Fixes
* do not delete output file ([0f3e090](https://github.com/ReVanced/revanced-cli/commit/0f3e090418771e951dfd15e5c193421f72cbe459))
# [3.0.0-dev.7](https://github.com/ReVanced/revanced-cli/compare/v3.0.0-dev.6...v3.0.0-dev.7) (2023-08-24)
### Bug Fixes
* print stack trace when a patch failed ([924c1f8](https://github.com/ReVanced/revanced-cli/commit/924c1f80ec0d17a3bdc07a0fb2015e44c49162e4))
# [3.0.0-dev.6](https://github.com/ReVanced/revanced-cli/compare/v3.0.0-dev.5...v3.0.0-dev.6) (2023-08-24)
# [3.0.0-dev.5](https://github.com/ReVanced/revanced-cli/compare/v3.0.0-dev.4...v3.0.0-dev.5) (2023-08-24)
### Bug Fixes
* also delete temporary files when uninstalling ([52c3be2](https://github.com/ReVanced/revanced-cli/commit/52c3be23f2915dccaee7f9941413c8f81e14acc8))
* delete temporary files after root installation ([a3d8705](https://github.com/ReVanced/revanced-cli/commit/a3d8705e89732a0dd4f51de28c405b6af13c8633))
* fix running commands not running ([2c7fcaf](https://github.com/ReVanced/revanced-cli/commit/2c7fcaf4add65a12052afc5bef779dbc73debd69))
* only check once for patch options ([11c3a6c](https://github.com/ReVanced/revanced-cli/commit/11c3a6cfd4fe59ba5d703358634a1853e1cc22a5))
### Features
* add install command ([0350b7f](https://github.com/ReVanced/revanced-cli/commit/0350b7f1a276d9dc795b22442ba4f202855ea090))
* use friendly descriptions ([3dd875d](https://github.com/ReVanced/revanced-cli/commit/3dd875d14cca488ade6d21bbd4cce0d481692134))
# [3.0.0-dev.4](https://github.com/ReVanced/revanced-cli/compare/v3.0.0-dev.3...v3.0.0-dev.4) (2023-08-24)
### Features
* properly make use of logging facade ([41898d7](https://github.com/ReVanced/revanced-cli/commit/41898d7547690e3130372414515c5645e5dc2634))
# [3.0.0-dev.3](https://github.com/ReVanced/revanced-cli/compare/v3.0.0-dev.2...v3.0.0-dev.3) (2023-08-23) # [3.0.0-dev.3](https://github.com/ReVanced/revanced-cli/compare/v3.0.0-dev.2...v3.0.0-dev.3) (2023-08-23)
# [3.0.0-dev.2](https://github.com/ReVanced/revanced-cli/compare/v3.0.0-dev.1...v3.0.0-dev.2) (2023-08-23) # [3.0.0-dev.2](https://github.com/ReVanced/revanced-cli/compare/v3.0.0-dev.1...v3.0.0-dev.2) (2023-08-23)

View File

@@ -52,5 +52,6 @@ tasks {
register<DefaultTask>("publish") { register<DefaultTask>("publish") {
group = "publish" group = "publish"
description = "Dummy task" description = "Dummy task"
dependsOn(build)
} }
} }

View File

@@ -86,12 +86,26 @@ Learn how to ReVanced CLI.
> **Note**: Some patches may require integrations > **Note**: Some patches may require integrations
such as [ReVanced Integrations](https://github.com/revanced/revanced-integrations). such as [ReVanced Integrations](https://github.com/revanced/revanced-integrations).
Supply them with the option `-m`. If any patches accepted by ReVanced Patcher require ReVanced Integrations, Supply them with the option `--merge`. If any patches accepted by ReVanced Patcher require ReVanced Integrations,
they will be merged into the APK file automatically. they will be merged into the APK file automatically.
- ### 🗑️ Uninstall a patched APK file - ### 🗑️ Uninstall a patched APK file
```bash ```bash
java -jar revanced-cli.jar uninstall \ java -jar revanced-cli.jar utility uninstall \
--package-name <package-name> \ --package-name <package-name> \
<device-serial> <device-serial>
``` ```
> **Note**: You can unmount an APK file
with the option `--unmount`.
- ### ⚙️ Manually install an APK file
```bash
java -jar revanced-cli.jar utility install \
-a input.apk \
<device-serial>
```
> **Note**: You can mount an APK file
by supplying the package name of the app to mount the supplied APK file to over the option `--mount`.

View File

@@ -1,4 +1,4 @@
org.gradle.parallel = true org.gradle.parallel = true
org.gradle.caching = true org.gradle.caching = true
kotlin.code.style = official kotlin.code.style = official
version = 3.0.0-dev.3 version = 3.0.0-dev.10

View File

@@ -8,7 +8,7 @@ kotlin-reflect = "1.9.0"
kotlin-test = "1.8.20-RC" kotlin-test = "1.8.20-RC"
kotlinx-coroutines-core = "1.7.1" kotlinx-coroutines-core = "1.7.1"
picocli = "4.7.3" picocli = "4.7.3"
revanced-patcher = "14.0.0" revanced-patcher = "14.1.0"
[libraries] [libraries]
apksig = { module = "com.android.tools.build:apksig", version.ref = "apksig" } apksig = { module = "com.android.tools.build:apksig", version.ref = "apksig" }

View File

@@ -11,43 +11,41 @@ import app.revanced.patcher.patch.PatchOption
import picocli.CommandLine.* import picocli.CommandLine.*
import picocli.CommandLine.Help.Visibility.ALWAYS import picocli.CommandLine.Help.Visibility.ALWAYS
import java.io.File import java.io.File
import java.util.logging.Logger
@Command(name = "list-patches", description = ["List patches from supplied patch bundles"]) @Command(name = "list-patches", description = ["List patches from supplied patch bundles"])
internal object ListPatchesCommand : Runnable { internal object ListPatchesCommand : Runnable {
private val logger = Logger.getLogger(ListPatchesCommand::class.java.name)
@Parameters( @Parameters(
description = ["Paths to patch bundles"], description = ["Paths to patch bundles"], arity = "1..*"
arity = "1..*"
) )
lateinit var patchBundles: Array<File> private lateinit var patchBundles: Array<File>
@Option( @Option(
names = ["-d", "--with-descriptions"], names = ["-d", "--with-descriptions"], description = ["List their descriptions"], showDefaultValue = ALWAYS
description = ["List their descriptions"],
showDefaultValue = ALWAYS
) )
var withDescriptions: Boolean = true private var withDescriptions: Boolean = true
@Option( @Option(
names = ["-p", "--with-packages"], names = ["-p", "--with-packages"],
description = ["List the packages the patches are compatible with"], description = ["List the packages the patches are compatible with"],
showDefaultValue = ALWAYS showDefaultValue = ALWAYS
) )
var withPackages: Boolean = false private var withPackages: Boolean = false
@Option( @Option(
names = ["-v", "--with-versions"], names = ["-v", "--with-versions"],
description = ["List the versions of the packages the patches are compatible with"], description = ["List the versions of the apps the patches are compatible with"],
showDefaultValue = ALWAYS showDefaultValue = ALWAYS
) )
var withVersions: Boolean = false private var withVersions: Boolean = false
@Option( @Option(
names = ["-o", "--with-options"], names = ["-o", "--with-options"], description = ["List the options of the patches"], showDefaultValue = ALWAYS
description = ["List the options of the patches"],
showDefaultValue = ALWAYS
) )
var withOptions: Boolean = false private var withOptions: Boolean = false
override fun run() { override fun run() {
fun Package.buildString() = buildString { fun Package.buildString() = buildString {
@@ -55,8 +53,7 @@ internal object ListPatchesCommand : Runnable {
appendLine("Package name: $name") appendLine("Package name: $name")
appendLine("Compatible versions:") appendLine("Compatible versions:")
append(versions.joinToString("\n") { version -> version }.prependIndent("\t")) append(versions.joinToString("\n") { version -> version }.prependIndent("\t"))
} else } else append("Package name: $name")
append("Package name: $name")
} }
fun PatchOption<*>.buildString() = buildString { fun PatchOption<*>.buildString() = buildString {

View File

@@ -1,21 +1,48 @@
package app.revanced.cli.command package app.revanced.cli.command
import app.revanced.cli.logging.impl.DefaultCliLogger import app.revanced.cli.command.utility.UtilityCommand
import app.revanced.patcher.patch.PatchClass import app.revanced.patcher.patch.PatchClass
import picocli.CommandLine import picocli.CommandLine
import picocli.CommandLine.Command import picocli.CommandLine.Command
import picocli.CommandLine.IVersionProvider import picocli.CommandLine.IVersionProvider
import java.util.* import java.util.*
import java.util.logging.*
fun main(args: Array<String>) { fun main(args: Array<String>) {
System.setProperty("java.util.logging.SimpleFormatter.format", "%4\$s: %5\$s %n")
Logger.getLogger("").apply {
handlers.forEach {
it.close()
removeHandler(it)
}
object : Handler() {
override fun publish(record: LogRecord) = formatter.format(record).toByteArray().let {
if (record.level.intValue() > Level.INFO.intValue())
System.err.write(it)
else
System.out.write(it)
}
override fun flush() {
System.out.flush()
System.err.flush()
}
override fun close() = flush()
}.also {
it.level = Level.ALL
it.formatter = SimpleFormatter()
}.let(::addHandler)
}
CommandLine(MainCommand).execute(*args) CommandLine(MainCommand).execute(*args)
} }
internal typealias PatchList = List<PatchClass> internal typealias PatchList = List<PatchClass>
internal val logger = DefaultCliLogger() private object CLIVersionProvider : IVersionProvider {
object CLIVersionProvider : IVersionProvider {
override fun getVersion(): Array<String> { override fun getVersion(): Array<String> {
Properties().apply { Properties().apply {
load(MainCommand::class.java.getResourceAsStream("/app/revanced/cli/version.properties")) load(MainCommand::class.java.getResourceAsStream("/app/revanced/cli/version.properties"))
@@ -33,8 +60,8 @@ object CLIVersionProvider : IVersionProvider {
subcommands = [ subcommands = [
ListPatchesCommand::class, ListPatchesCommand::class,
PatchCommand::class, PatchCommand::class,
UninstallCommand::class,
OptionsCommand::class, OptionsCommand::class,
UtilityCommand::class,
] ]
) )
internal object MainCommand private object MainCommand

View File

@@ -6,45 +6,41 @@ import app.revanced.utils.Options.setOptions
import picocli.CommandLine import picocli.CommandLine
import picocli.CommandLine.Help.Visibility.ALWAYS import picocli.CommandLine.Help.Visibility.ALWAYS
import java.io.File import java.io.File
import java.util.logging.Logger
@CommandLine.Command( @CommandLine.Command(
name = "options", name = "options",
description = ["Generate options file from patches"], description = ["Generate options file from patches"],
) )
internal object OptionsCommand : Runnable { internal object OptionsCommand : Runnable {
private val logger = Logger.getLogger(OptionsCommand::class.java.name)
@CommandLine.Parameters( @CommandLine.Parameters(
description = ["Paths to patch bundles"], description = ["Paths to patch bundles"], arity = "1..*"
arity = "1..*"
) )
lateinit var patchBundles: Array<File> private lateinit var patchBundles: Array<File>
@CommandLine.Option( @CommandLine.Option(
names = ["-p", "--path"], names = ["-p", "--path"], description = ["Path to patch options JSON file"], showDefaultValue = ALWAYS
description = ["Path to patch options JSON file"],
showDefaultValue = ALWAYS
) )
var path: File = File("options.json") private var path: File = File("options.json")
@CommandLine.Option( @CommandLine.Option(
names = ["-o", "--overwrite"], names = ["-o", "--overwrite"], description = ["Overwrite existing options file"], showDefaultValue = ALWAYS
description = ["Overwrite existing options file"],
showDefaultValue = ALWAYS
) )
var overwrite: Boolean = false private var overwrite: Boolean = false
@CommandLine.Option( @CommandLine.Option(
names = ["-u", "--update"], names = ["-u", "--update"],
description = ["Update existing options by adding missing and removing non-existent options"], description = ["Update existing options by adding missing and removing non-existent options"],
showDefaultValue = ALWAYS showDefaultValue = ALWAYS
) )
var update: Boolean = false private var update: Boolean = false
override fun run() = if (!path.exists() || overwrite) override fun run() = if (!path.exists() || overwrite) with(PatchBundleLoader.Jar(*patchBundles)) {
with(PatchBundleLoader.Jar(*patchBundles)) { if (update) setOptions(path)
if (update) setOptions(path, logger)
Options.serialize(this, prettyPrint = true) Options.serialize(this, prettyPrint = true).let(path::writeText)
.let(path::writeText) }
} else logger.severe("Options file already exists, use --override to override it")
else logger.error("Options file already exists, use --override to override it")
} }

View File

@@ -1,6 +1,5 @@
package app.revanced.cli.command package app.revanced.cli.command
import app.revanced.cli.patcher.logging.impl.PatcherLogger
import app.revanced.patcher.PatchBundleLoader import app.revanced.patcher.PatchBundleLoader
import app.revanced.patcher.Patcher import app.revanced.patcher.Patcher
import app.revanced.patcher.PatcherOptions import app.revanced.patcher.PatcherOptions
@@ -20,85 +19,75 @@ import kotlinx.coroutines.runBlocking
import picocli.CommandLine import picocli.CommandLine
import picocli.CommandLine.Help.Visibility.ALWAYS import picocli.CommandLine.Help.Visibility.ALWAYS
import java.io.File import java.io.File
import java.io.PrintWriter
import java.io.StringWriter
import java.util.logging.Logger
@CommandLine.Command( @CommandLine.Command(
name = "patch", name = "patch", description = ["Patch the supplied APK file with the supplied patches and integrations"]
description = ["Patch the supplied APK file with the supplied patches and integrations"]
) )
internal object PatchCommand: Runnable { internal object PatchCommand : Runnable {
private val logger = Logger.getLogger(PatchCommand::class.java.name)
@CommandLine.Parameters( @CommandLine.Parameters(
description = ["APK file to be patched"], description = ["APK file to be patched"], arity = "1..1"
arity = "1..1"
) )
lateinit var apk: File private lateinit var apk: File
@CommandLine.Option( @CommandLine.Option(
names = ["-b", "--patch-bundle"], names = ["-b", "--patch-bundle"], description = ["One or more bundles of patches"], required = true
description = ["One or more bundles of patches"],
required = true
) )
var patchBundles = emptyList<File>() private var patchBundles = emptyList<File>()
@CommandLine.Option( @CommandLine.Option(
names = ["-m", "--merge"], names = ["-m", "--merge"], description = ["One or more DEX files or containers to merge into the APK"]
description = ["One or more DEX files or containers to merge into the APK"]
) )
var integrations = listOf<File>() private var integrations = listOf<File>()
@CommandLine.Option( @CommandLine.Option(
names = ["-i", "--include"], names = ["-i", "--include"], description = ["List of patches to include"]
description = ["List of patches to include"]
) )
var includedPatches = arrayOf<String>() private var includedPatches = arrayOf<String>()
@CommandLine.Option( @CommandLine.Option(
names = ["-e", "--exclude"], names = ["-e", "--exclude"], description = ["List of patches to exclude"]
description = ["List of patches to exclude"]
) )
var excludedPatches = arrayOf<String>() private var excludedPatches = arrayOf<String>()
@CommandLine.Option( @CommandLine.Option(
names = ["--options"], names = ["--options"], description = ["Path to patch options JSON file"], showDefaultValue = ALWAYS
description = ["Path to patch options JSON file"],
showDefaultValue = ALWAYS
) )
var optionsFile: File = File("options.json") private var optionsFile: File = File("options.json")
@CommandLine.Option( @CommandLine.Option(
names = ["--exclusive"], names = ["--exclusive"],
description = ["Only include patches that are explicitly specified to be included"], description = ["Only include patches that are explicitly specified to be included"],
showDefaultValue = ALWAYS showDefaultValue = ALWAYS
) )
var exclusive = false private var exclusive = false
@CommandLine.Option( @CommandLine.Option(
names = ["--experimental"], names = ["-f","--force"],
description = ["Ignore patches incompatibility to versions"], description = ["Force inclusion of patches that are incompatible with the supplied APK file's version"],
showDefaultValue = ALWAYS showDefaultValue = ALWAYS
) )
var experimental: Boolean = false private var force: Boolean = false
@CommandLine.Option( @CommandLine.Option(
names = ["-o", "--out"], names = ["-o", "--out"], description = ["Path to save the patched APK file to"], required = true
description = ["Path to save the patched APK file to"],
required = true
) )
lateinit var outputFilePath: File private lateinit var outputFilePath: File
@CommandLine.Option( @CommandLine.Option(
names = ["-d", "--device-serial"], names = ["-d", "--device-serial"], description = ["ADB device serial to install to"], showDefaultValue = ALWAYS
description = ["ADB device serial to install to"],
showDefaultValue = ALWAYS
) )
var deviceSerial: String? = null private var deviceSerial: String? = null
@CommandLine.Option( @CommandLine.Option(
names = ["--mount"], names = ["--mount"], description = ["Install by mounting the patched APK file"], showDefaultValue = ALWAYS
description = ["Install by mounting the patched package"],
showDefaultValue = ALWAYS
) )
var mount: Boolean = false private var mount: Boolean = false
@CommandLine.Option( @CommandLine.Option(
names = ["--common-name"], names = ["--common-name"],
@@ -106,53 +95,57 @@ internal object PatchCommand: Runnable {
showDefaultValue = ALWAYS showDefaultValue = ALWAYS
) )
var commonName = "ReVanced" private var commonName = "ReVanced"
@CommandLine.Option( @CommandLine.Option(
names = ["--keystore"], names = ["--keystore"], description = ["Path to the keystore to sign the patched APK file with"]
description = ["Path to the keystore to sign the patched APK file with"]
) )
var keystorePath: String? = null private var keystorePath: String? = null
@CommandLine.Option( @CommandLine.Option(
names = ["--password"], names = ["--password"], description = ["The password of the keystore to sign the patched APK file with"]
description = ["The password of the keystore to sign the patched APK file with"]
) )
var password = "ReVanced" private var password = "ReVanced"
@CommandLine.Option( @CommandLine.Option(
names = ["-r", "--resource-cache"], names = ["-r", "--resource-cache"],
description = ["Path to temporary resource cache directory"], description = ["Path to temporary resource cache directory"],
showDefaultValue = ALWAYS showDefaultValue = ALWAYS
) )
var resourceCachePath = File("revanced-resource-cache") private var resourceCachePath = File("revanced-resource-cache")
@CommandLine.Option( @CommandLine.Option(
names = ["--custom-aapt2-binary"], names = ["--custom-aapt2-binary"], description = ["Path to a custom AAPT binary to compile resources with"]
description = ["Path to a custom AAPT binary to compile resources with"]
) )
var aaptBinaryPath = File("") private var aaptBinaryPath = File("")
@CommandLine.Option( @CommandLine.Option(
names = ["-p", "--purge"], names = ["-p", "--purge"],
description = ["Purge the temporary resource cache directory after patching"], description = ["Purge the temporary resource cache directory after patching"],
showDefaultValue = ALWAYS showDefaultValue = ALWAYS
) )
var purge: Boolean = false private var purge: Boolean = false
override fun run() { override fun run() {
// region Prepare // region Prepare
if (!apk.exists()) { if (!apk.exists()) {
logger.error("Input file ${apk.name} does not exist") logger.severe("APK file ${apk.name} does not exist")
return
}
integrations.filter { !it.exists() }.let {
if (it.isEmpty()) return@let
it.forEach { integration ->
logger.severe("Integration file ${integration.name} does not exist")
}
return return
} }
val adbManager = deviceSerial?.let { serial -> val adbManager = deviceSerial?.let { serial ->
if (mount) AdbManager.RootAdbManager(serial, logger) else AdbManager.UserAdbManager( if (mount) AdbManager.RootAdbManager(serial)
serial, else AdbManager.UserAdbManager(serial)
logger
)
} }
// endregion // endregion
@@ -167,7 +160,7 @@ internal object PatchCommand: Runnable {
logger.info("Setting patch options") logger.info("Setting patch options")
optionsFile.let { optionsFile.let {
if (it.exists()) patches.setOptions(it, logger) if (it.exists()) patches.setOptions(it)
else Options.serialize(patches, prettyPrint = true).let(it::writeText) else Options.serialize(patches, prettyPrint = true).let(it::writeText)
} }
@@ -181,7 +174,6 @@ internal object PatchCommand: Runnable {
resourceCachePath, resourceCachePath,
aaptBinaryPath.path, aaptBinaryPath.path,
resourceCachePath.absolutePath, resourceCachePath.absolutePath,
PatcherLogger
) )
) )
@@ -193,7 +185,10 @@ internal object PatchCommand: Runnable {
runBlocking { runBlocking {
apply(false).collect { patchResult -> apply(false).collect { patchResult ->
patchResult.exception?.let { patchResult.exception?.let {
logger.error("${patchResult.patchName} failed:\n${patchResult.exception}") StringWriter().use { writer ->
it.printStackTrace(PrintWriter(writer))
logger.severe("${patchResult.patchName} failed: $writer")
}
} ?: logger.info("${patchResult.patchName} succeeded") } ?: logger.info("${patchResult.patchName} succeeded")
} }
} }
@@ -207,8 +202,7 @@ internal object PatchCommand: Runnable {
val alignAndSignedFile = sign( val alignAndSignedFile = sign(
apk.newAlignedFile( apk.newAlignedFile(
result, result, resourceCachePath.resolve("${outputFilePath.nameWithoutExtension}_aligned.apk")
resourceCachePath.resolve("${outputFilePath.nameWithoutExtension}_aligned.apk")
) )
) )
@@ -219,7 +213,6 @@ internal object PatchCommand: Runnable {
if (purge) { if (purge) {
logger.info("Purging temporary files") logger.info("Purging temporary files")
outputFilePath.delete()
purge(resourceCachePath) purge(resourceCachePath)
} }
@@ -232,8 +225,8 @@ internal object PatchCommand: Runnable {
* - [includedPatches] (explicitly included) * - [includedPatches] (explicitly included)
* - [excludedPatches] (explicitly excluded) * - [excludedPatches] (explicitly excluded)
* - [exclusive] (only include patches that are explicitly included) * - [exclusive] (only include patches that are explicitly included)
* - [experimental] (ignore patches incompatibility to versions) * - [force] (ignore patches incompatibility to versions)
* - package name and version of the input APK file (if [experimental] is false) * - Package name and version of the input APK file (if [force] is false)
* *
* @param patches The patches to filter. * @param patches The patches to filter.
* @return The filtered patches. * @return The filtered patches.
@@ -245,91 +238,39 @@ internal object PatchCommand: Runnable {
patches.forEach patch@{ patch -> patches.forEach patch@{ patch ->
val formattedPatchName = patch.patchName.lowercase().replace(" ", "-") val formattedPatchName = patch.patchName.lowercase().replace(" ", "-")
/** val explicitlyExcluded = excludedPatches.contains(formattedPatchName)
* Check if the patch is explicitly excluded. if (explicitlyExcluded) return@patch logger.info("Excluding ${patch.patchName}")
*
* Cases:
* 1. -e patch.name
* 2. -i patch.name -e patch.name
*/
/** // If the patch is explicitly included, it will be included if [exclusive] is false.
* Check if the patch is explicitly excluded. val explicitlyIncluded = exclusive && includedPatches.contains(formattedPatchName)
*
* Cases:
* 1. -e patch.name
* 2. -i patch.name -e patch.name
*/
val excluded = excludedPatches.contains(formattedPatchName) // If the patch is implicitly included, it will be only included if [exclusive] is false.
if (excluded) return@patch logger.info("Excluding ${patch.patchName}") val implicitlyIncluded = !exclusive && patch.include
/** val included = implicitlyIncluded || explicitlyIncluded
* Check if the patch is constrained to packages. if (!included) return@patch logger.info("${patch.patchName} excluded by default") // Case 1.
*/
/**
* Check if the patch is constrained to packages.
*/
// At last make sure the patch is compatible with the supplied APK files package name and version.
patch.compatiblePackages?.let { packages -> patch.compatiblePackages?.let { packages ->
packages.singleOrNull { it.name == packageName }?.let { `package` -> packages.singleOrNull { it.name == packageName }?.let { `package` ->
/** val matchesVersion = force || `package`.versions.let {
* Check if the package version matches.
* If experimental is true, version matching will be skipped.
*/
/**
* Check if the package version matches.
* If experimental is true, version matching will be skipped.
*/
val matchesVersion = experimental || `package`.versions.let {
it.isEmpty() || it.any { version -> version == packageVersion } it.isEmpty() || it.any { version -> version == packageVersion }
} }
if (!matchesVersion) return@patch logger.warn( if (!matchesVersion) return@patch logger.warning("${patch.patchName} is incompatible with version $packageVersion. " + "This patch is only compatible with version " + packages.joinToString(
"${patch.patchName} is incompatible with version $packageVersion. " + ";"
"This patch is only compatible with version " + ) { pkg ->
packages.joinToString(";") { pkg -> "${pkg.name}: ${pkg.versions.joinToString(", ")}"
"${pkg.name}: ${pkg.versions.joinToString(", ")}" })
}
)
} ?: return@patch logger.trace( } ?: return@patch logger.fine("${patch.patchName} is incompatible with $packageName. "
"${patch.patchName} is incompatible with $packageName. " + + "This patch is only compatible with "
"This patch is only compatible with " + + packages.joinToString(", ") { `package` -> `package`.name })
packages.joinToString(", ") { `package` -> `package`.name }
)
return@let return@let
} ?: logger.trace("$formattedPatchName: No constraint on packages.") } ?: logger.fine("$formattedPatchName: No constraint on packages.")
/** logger.fine("Adding $formattedPatchName")
* Check if the patch is explicitly included.
*
* Cases:
* 1. --exclusive
* 2. --exclusive -i patch.name
*/
/**
* Check if the patch is explicitly included.
*
* Cases:
* 1. --exclusive
* 2. --exclusive -i patch.name
*/
val explicitlyIncluded = 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) add(patch)
} }
@@ -342,8 +283,7 @@ internal object PatchCommand: Runnable {
* @param outputFile The file to save the aligned APK to. * @param outputFile The file to save the aligned APK to.
*/ */
private fun File.newAlignedFile( private fun File.newAlignedFile(
result: PatcherResult, result: PatcherResult, outputFile: File
outputFile: File
): File { ): File {
logger.info("Aligning $name") logger.info("Aligning $name")
@@ -352,23 +292,20 @@ internal object PatchCommand: Runnable {
ZipFile(outputFile).use { file -> ZipFile(outputFile).use { file ->
result.dexFiles.forEach { result.dexFiles.forEach {
file.addEntryCompressData( file.addEntryCompressData(
ZipEntry.createWithName(it.name), ZipEntry.createWithName(it.name), it.stream.readBytes()
it.stream.readBytes()
) )
} }
result.resourceFile?.let { result.resourceFile?.let {
file.copyEntriesFromFileAligned( file.copyEntriesFromFileAligned(
ZipFile(it), ZipFile(it), ZipAligner::getEntryAlignment
ZipAligner::getEntryAlignment
) )
} }
// TODO: Do not compress result.doNotCompress // TODO: Do not compress result.doNotCompress
file.copyEntriesFromFileAligned( file.copyEntriesFromFileAligned(
ZipFile(this), ZipFile(this), ZipAligner::getEntryAlignment
ZipAligner::getEntryAlignment
) )
} }
@@ -381,32 +318,25 @@ internal object PatchCommand: Runnable {
* @param inputFile The APK file to sign. * @param inputFile The APK file to sign.
* @return The signed APK file. If [mount] is true, the input file will be returned. * @return The signed APK file. If [mount] is true, the input file will be returned.
*/ */
private fun sign(inputFile: File) = if (mount) private fun sign(inputFile: File) = if (mount) inputFile
inputFile
else { else {
logger.info("Signing ${inputFile.name}") logger.info("Signing ${inputFile.name}")
val keyStoreFilePath = keystorePath ?: outputFilePath val keyStoreFilePath = keystorePath
.absoluteFile.parentFile.resolve("${outputFilePath.nameWithoutExtension}.keystore").canonicalPath ?: outputFilePath.absoluteFile.parentFile.resolve("${outputFilePath.nameWithoutExtension}.keystore").canonicalPath
val options = SigningOptions( val options = SigningOptions(
commonName, commonName, password, keyStoreFilePath
password,
keyStoreFilePath
) )
ApkSigner(options) ApkSigner(options).signApk(
.signApk( inputFile, resourceCachePath.resolve("${outputFilePath.nameWithoutExtension}_signed.apk")
inputFile,
resourceCachePath.resolve("${outputFilePath.nameWithoutExtension}_signed.apk")
) )
} }
private fun purge(resourceCachePath: File) { private fun purge(resourceCachePath: File) {
val result = if (resourceCachePath.deleteRecursively()) val result = if (resourceCachePath.deleteRecursively()) "Purged resource cache directory"
"Purged resource cache directory" else "Failed to purge resource cache directory"
else
"Failed to purge resource cache directory"
logger.info(result) logger.info(result)
} }
} }

View File

@@ -1,44 +0,0 @@
package app.revanced.cli.command
import app.revanced.utils.adb.AdbManager
import picocli.CommandLine.*
import picocli.CommandLine.Help.Visibility.ALWAYS
@Command(
name = "uninstall",
description = ["Uninstall a patched APK file from the devices with the supplied ADB device serials"]
)
internal object UninstallCommand : Runnable {
@Parameters(
description = ["ADB device serials"],
arity = "1..*"
)
lateinit var deviceSerials: Array<String>
@Option(
names = ["-p", "--package-name"],
description = ["Package name to uninstall"],
required = true
)
lateinit var packageName: String
@Option(
names = ["-u", "--unmount"],
description = ["Uninstall by unmounting the patched package"],
showDefaultValue = ALWAYS
)
var unmount: Boolean = false
override fun run() = try {
deviceSerials.forEach {deviceSerial ->
if (unmount) {
AdbManager.RootAdbManager(deviceSerial, logger)
} else {
AdbManager.UserAdbManager(deviceSerial, logger)
}.uninstall(packageName)
}
} catch (e: AdbManager.DeviceNotFoundException) {
logger.error(e.toString())
}
}

View File

@@ -0,0 +1,42 @@
package app.revanced.cli.command.utility
import app.revanced.utils.adb.AdbManager
import picocli.CommandLine.*
import java.io.File
import java.util.logging.Logger
@Command(
name = "install", description = ["Install an APK file to devices with the supplied ADB device serials"]
)
internal object InstallCommand : Runnable {
private val logger = Logger.getLogger(InstallCommand::class.java.name)
@Parameters(
description = ["ADB device serials"], arity = "1..*"
)
private lateinit var deviceSerials: Array<String>
@Option(
names = ["-a", "--apk"], description = ["APK file to be installed"], required = true
)
private lateinit var apk: File
@Option(
names = ["-m", "--mount"],
description = ["Mount the supplied APK file over the app with the supplied package name"],
)
private var packageName: String? = null
override fun run() = try {
deviceSerials.forEach { deviceSerial ->
if (packageName != null) {
AdbManager.RootAdbManager(deviceSerial)
} else {
AdbManager.UserAdbManager(deviceSerial)
}.install(AdbManager.Apk(apk, packageName))
}
} catch (e: AdbManager.DeviceNotFoundException) {
logger.severe(e.toString())
}
}

View File

@@ -0,0 +1,40 @@
package app.revanced.cli.command.utility
import app.revanced.utils.adb.AdbManager
import picocli.CommandLine.*
import picocli.CommandLine.Help.Visibility.ALWAYS
import java.util.logging.Logger
@Command(
name = "uninstall",
description = ["Uninstall a patched app from the devices with the supplied ADB device serials"]
)
internal object UninstallCommand : Runnable {
private val logger = Logger.getLogger(UninstallCommand::class.java.name)
@Parameters(description = ["ADB device serials"], arity = "1..*")
private lateinit var deviceSerials: Array<String>
@Option(names = ["-p", "--package-name"], description = ["Package name of the app to uninstall"], required = true)
private lateinit var packageName: String
@Option(
names = ["-u", "--unmount"],
description = ["Uninstall by unmounting the patched APK file"],
showDefaultValue = ALWAYS
)
private var unmount: Boolean = false
override fun run() = try {
deviceSerials.forEach { deviceSerial ->
if (unmount) {
AdbManager.RootAdbManager(deviceSerial)
} else {
AdbManager.UserAdbManager(deviceSerial)
}.uninstall(packageName)
}
} catch (e: AdbManager.DeviceNotFoundException) {
logger.severe(e.toString())
}
}

View File

@@ -0,0 +1,10 @@
package app.revanced.cli.command.utility
import picocli.CommandLine
@CommandLine.Command(
name = "utility",
description = ["Commands for utility purposes"],
subcommands = [InstallCommand::class, UninstallCommand::class],
)
internal object UtilityCommand

View File

@@ -1,8 +0,0 @@
package app.revanced.cli.logging
internal interface CliLogger {
fun error(msg: String)
fun info(msg: String)
fun trace(msg: String)
fun warn(msg: String)
}

View File

@@ -1,30 +0,0 @@
package app.revanced.cli.logging.impl
import app.revanced.cli.command.MainCommand
import app.revanced.cli.logging.CliLogger
import java.util.logging.Logger
import java.util.logging.SimpleFormatter
internal class DefaultCliLogger(
private val logger: Logger = Logger.getLogger(MainCommand::class.java.name),
private val errorLogger: Logger = Logger.getLogger(logger.name + "Err")
) : CliLogger {
init {
logger.useParentHandlers = false
if (logger.handlers.isEmpty()) {
logger.addHandler(FlushingStreamHandler(System.out, SimpleFormatter()))
}
}
companion object {
init {
System.setProperty("java.util.logging.SimpleFormatter.format", "%4\$s: %5\$s %n")
}
}
override fun error(msg: String) = errorLogger.severe(msg)
override fun info(msg: String) = logger.info(msg)
override fun trace(msg: String) = logger.finest(msg)
override fun warn(msg: String) = errorLogger.warning(msg)
}

View File

@@ -1,13 +0,0 @@
package app.revanced.cli.logging.impl
import java.io.OutputStream
import java.util.logging.Formatter
import java.util.logging.LogRecord
import java.util.logging.StreamHandler
internal class FlushingStreamHandler(out: OutputStream, format: Formatter) : StreamHandler(out, format) {
override fun publish(record: LogRecord) {
super.publish(record)
flush()
}
}

View File

@@ -1,13 +0,0 @@
package app.revanced.cli.patcher.logging.impl
import app.revanced.cli.logging.impl.DefaultCliLogger
import java.util.logging.Logger
internal object PatcherLogger : app.revanced.patcher.logging.Logger{
private val logger = DefaultCliLogger(Logger.getLogger(app.revanced.patcher.Patcher::class.java.name))
override fun error(msg: String) = logger.error(msg)
override fun info(msg: String) = logger.info(msg)
override fun warn(msg: String)= logger.warn(msg)
override fun trace(msg: String)= logger.trace(msg)
}

View File

@@ -1,16 +1,18 @@
package app.revanced.utils package app.revanced.utils
import app.revanced.cli.command.PatchList import app.revanced.cli.command.PatchList
import app.revanced.cli.logging.CliLogger
import app.revanced.patcher.extensions.PatchExtensions.options import app.revanced.patcher.extensions.PatchExtensions.options
import app.revanced.patcher.extensions.PatchExtensions.patchName import app.revanced.patcher.extensions.PatchExtensions.patchName
import app.revanced.patcher.patch.NoSuchOptionException import app.revanced.patcher.patch.NoSuchOptionException
import app.revanced.utils.Options.PatchOption.Option import app.revanced.utils.Options.PatchOption.Option
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import java.io.File import java.io.File
import java.util.logging.Logger
internal object Options { internal object Options {
private val logger = Logger.getLogger(Options::class.java.name)
private var mapper = jacksonObjectMapper() private var mapper = jacksonObjectMapper()
/** /**
@@ -53,22 +55,24 @@ internal object Options {
* Sets the options for the patches in the list. * Sets the options for the patches in the list.
* *
* @param json The JSON string containing the options. * @param json The JSON string containing the options.
* @param logger The logger to use for logging.
*/ */
fun PatchList.setOptions(json: String, logger: CliLogger? = null) { fun PatchList.setOptions(json: String) {
filter { it.options?.any() == true }.let { patches -> filter { it.options?.any() == true }.let { patches ->
if (patches.isEmpty()) return if (patches.isEmpty()) return
val patchOptions = deserialize(json) val patchOptions = deserialize(json)
patches.forEach { patch -> patches.forEach patch@{ patch ->
patchOptions.find { option -> option.patchName == patch.patchName }?.let { patchOptions.find { option -> option.patchName == patch.patchName }?.let {
it.options.forEach { option -> it.options.forEach { option ->
try { try {
patch.options?.set(option.key, option.value) patch.options?.set(option.key, option.value)
?: logger?.warn("${patch.patchName} has no options") ?: run{
logger.warning("${patch.patchName} has no options")
return@patch
}
} catch (e: NoSuchOptionException) { } catch (e: NoSuchOptionException) {
logger?.error(e.message ?: "Unknown error") logger.info(e.toString())
} }
} }
} }
@@ -80,10 +84,9 @@ internal object Options {
* Sets the options for the patches in the list. * Sets the options for the patches in the list.
* *
* @param file The file containing the JSON string containing the options. * @param file The file containing the JSON string containing the options.
* @param logger The logger to use for logging.
* @see setOptions * @see setOptions
*/ */
fun PatchList.setOptions(file: File, logger: CliLogger? = null) = setOptions(file.readText(), logger) fun PatchList.setOptions(file: File) = setOptions(file.readText())
/** /**
* Data class for a patch and its [Option]s. * Data class for a patch and its [Option]s.

View File

@@ -1,36 +1,38 @@
package app.revanced.utils.adb package app.revanced.utils.adb
import app.revanced.cli.logging.CliLogger
import app.revanced.utils.adb.AdbManager.Apk import app.revanced.utils.adb.AdbManager.Apk
import app.revanced.utils.adb.Constants.COMMAND_CREATE_DIR import app.revanced.utils.adb.Constants.CREATE_DIR
import app.revanced.utils.adb.Constants.COMMAND_DELETE import app.revanced.utils.adb.Constants.DELETE
import app.revanced.utils.adb.Constants.COMMAND_INSTALL_MOUNT import app.revanced.utils.adb.Constants.INSTALLATION_PATH
import app.revanced.utils.adb.Constants.COMMAND_PREPARE_MOUNT_APK import app.revanced.utils.adb.Constants.INSTALL_MOUNT
import app.revanced.utils.adb.Constants.COMMAND_RESTART import app.revanced.utils.adb.Constants.INSTALL_PATCHED_APK
import app.revanced.utils.adb.Constants.COMMAND_UMOUNT import app.revanced.utils.adb.Constants.MOUNT_PATH
import app.revanced.utils.adb.Constants.CONTENT_MOUNT_SCRIPT import app.revanced.utils.adb.Constants.MOUNT_SCRIPT
import app.revanced.utils.adb.Constants.PATH_INIT_PUSH import app.revanced.utils.adb.Constants.PATCHED_APK_PATH
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 app.revanced.utils.adb.Constants.PLACEHOLDER
import app.revanced.utils.adb.Constants.RESTART
import app.revanced.utils.adb.Constants.TMP_PATH
import app.revanced.utils.adb.Constants.UMOUNT
import se.vidstige.jadb.JadbConnection import se.vidstige.jadb.JadbConnection
import se.vidstige.jadb.managers.Package import se.vidstige.jadb.managers.Package
import se.vidstige.jadb.managers.PackageManager import se.vidstige.jadb.managers.PackageManager
import java.io.Closeable import java.io.Closeable
import java.io.File import java.io.File
import java.util.logging.Logger
/** /**
* Adb manager. Used to install and uninstall [Apk] files. * Adb manager. Used to install and uninstall [Apk] files.
* *
* @param deviceSerial The serial of the device. * @param deviceSerial The serial of the device.
*/ */
internal sealed class AdbManager(deviceSerial: String? = null, protected val logger: CliLogger? = null) : Closeable { internal sealed class AdbManager(deviceSerial: String? = null) : Closeable {
protected val logger: Logger = Logger.getLogger(AdbManager::class.java.name)
protected val device = JadbConnection().devices.find { device -> device.serial == deviceSerial } protected val device = JadbConnection().devices.find { device -> device.serial == deviceSerial }
?: throw DeviceNotFoundException(deviceSerial) ?: throw DeviceNotFoundException(deviceSerial)
init { init {
logger?.trace("Established connection to $deviceSerial") logger.fine("Established connection to $deviceSerial")
} }
/** /**
@@ -39,7 +41,7 @@ internal sealed class AdbManager(deviceSerial: String? = null, protected val log
* @param apk The [Apk] file. * @param apk The [Apk] file.
*/ */
open fun install(apk: Apk) { open fun install(apk: Apk) {
logger?.info("Finished installing ${apk.file.name}") logger.info("Finished installing ${apk.file.name}")
} }
/** /**
@@ -48,51 +50,53 @@ internal sealed class AdbManager(deviceSerial: String? = null, protected val log
* @param packageName The package name. * @param packageName The package name.
*/ */
open fun uninstall(packageName: String) { open fun uninstall(packageName: String) {
logger?.info("Finished uninstalling $packageName") logger.info("Finished uninstalling $packageName")
} }
/** /**
* Closes the [AdbManager] instance. * Closes the [AdbManager] instance.
*/ */
override fun close() { override fun close() {
logger?.trace("Closed") logger.fine("Closed")
} }
class RootAdbManager(deviceSerial: String, logger: CliLogger? = null) : AdbManager(deviceSerial, logger) { class RootAdbManager(deviceSerial: String) : AdbManager(deviceSerial) {
init { init {
if (!device.hasSu()) throw IllegalArgumentException("Root required on $deviceSerial. Task failed") if (!device.hasSu()) throw IllegalArgumentException("Root required on $deviceSerial. Task failed")
} }
override fun install(apk: Apk) { override fun install(apk: Apk) {
logger?.info("Installing by mounting") logger.info("Installing by mounting")
val applyReplacement = getPlaceholderReplacement( val applyReplacement = getPlaceholderReplacement(
apk.packageName ?: throw IllegalArgumentException("Package name is required") apk.packageName ?: throw IllegalArgumentException("Package name is required")
) )
device.copyFile(apk.file, PATH_INIT_PUSH) device.push(apk.file, TMP_PATH)
device.run("$COMMAND_CREATE_DIR $PATH_INSTALLATION") device.run("$CREATE_DIR $INSTALLATION_PATH")
device.run(COMMAND_PREPARE_MOUNT_APK.applyReplacement()) device.run(INSTALL_PATCHED_APK.applyReplacement())
device.createFile(PATH_INIT_PUSH, CONTENT_MOUNT_SCRIPT.applyReplacement()) device.createFile(TMP_PATH, MOUNT_SCRIPT.applyReplacement())
device.run(COMMAND_INSTALL_MOUNT.applyReplacement()) device.run(INSTALL_MOUNT.applyReplacement())
device.run(COMMAND_UMOUNT.applyReplacement()) // Sanity check. device.run(UMOUNT.applyReplacement()) // Sanity check.
device.run(PATH_MOUNT.applyReplacement()) device.run(MOUNT_PATH.applyReplacement())
device.run(COMMAND_RESTART.applyReplacement()) device.run(RESTART.applyReplacement())
device.run(DELETE.applyReplacement(TMP_PATH).applyReplacement())
super.install(apk) super.install(apk)
} }
override fun uninstall(packageName: String) { override fun uninstall(packageName: String) {
logger?.info("Uninstalling $packageName by unmounting and deleting the package") logger.info("Uninstalling $packageName by unmounting")
val applyReplacement = getPlaceholderReplacement(packageName) val applyReplacement = getPlaceholderReplacement(packageName)
device.run(COMMAND_UMOUNT.applyReplacement(packageName)) device.run(UMOUNT.applyReplacement(packageName))
device.run(COMMAND_DELETE.applyReplacement(PATH_PATCHED_APK).applyReplacement()) device.run(DELETE.applyReplacement(PATCHED_APK_PATH).applyReplacement())
device.run(COMMAND_DELETE.applyReplacement(PATH_MOUNT).applyReplacement()) device.run(DELETE.applyReplacement(MOUNT_PATH).applyReplacement())
device.run(DELETE.applyReplacement(TMP_PATH).applyReplacement())
super.uninstall(packageName) super.uninstall(packageName)
} }
@@ -103,7 +107,7 @@ internal sealed class AdbManager(deviceSerial: String? = null, protected val log
} }
} }
class UserAdbManager(deviceSerial: String, logger: CliLogger? = null) : AdbManager(deviceSerial, logger) { class UserAdbManager(deviceSerial: String) : AdbManager(deviceSerial) {
private val packageManager = PackageManager(device) private val packageManager = PackageManager(device)
override fun install(apk: Apk) { override fun install(apk: Apk) {
@@ -113,7 +117,7 @@ internal sealed class AdbManager(deviceSerial: String? = null, protected val log
} }
override fun uninstall(packageName: String) { override fun uninstall(packageName: String) {
logger?.info("Uninstalling $packageName") logger.info("Uninstalling $packageName")
packageManager.uninstall(Package(packageName)) packageManager.uninstall(Package(packageName))
@@ -125,6 +129,7 @@ internal sealed class AdbManager(deviceSerial: String? = null, protected val log
* Apk file for [AdbManager]. * Apk file for [AdbManager].
* *
* @param file The [Apk] file. * @param file The [Apk] file.
* @param packageName The package name of the [Apk] file.
*/ */
internal class Apk(val file: File, val packageName: String? = null) internal class Apk(val file: File, val packageName: String? = null)

View File

@@ -2,24 +2,28 @@ package app.revanced.utils.adb
import se.vidstige.jadb.JadbDevice import se.vidstige.jadb.JadbDevice
import se.vidstige.jadb.RemoteFile import se.vidstige.jadb.RemoteFile
import se.vidstige.jadb.ShellProcessBuilder
import java.io.File import java.io.File
import java.util.concurrent.Callable
import java.util.concurrent.Executors
// 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)) { internal fun JadbDevice.buildCommand(command: String, su: Boolean = true): ShellProcessBuilder {
Executors.newFixedThreadPool(2).let { service -> if (su) return shellProcessBuilder("su -c \'$command\'")
arrayOf(inputStream, errorStream).map { stream ->
Callable { stream.bufferedReader().use { it.readLine() } } val args = command.split(" ") as ArrayList<String>
}.let { tasks -> service.invokeAny(tasks).also { service.shutdown() } } 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() = internal fun JadbDevice.hasSu() =
this.startCommand("su -h", false).waitFor() == 0 this.startCommand("su -h", false).waitFor() == 0
internal fun JadbDevice.copyFile(file: File, targetFile: String) = internal fun JadbDevice.push(file: File, targetFilePath: String) =
push(file, RemoteFile(targetFile)) push(file, RemoteFile(targetFilePath))
internal fun JadbDevice.createFile(targetFile: String, content: String) = internal fun JadbDevice.createFile(targetFile: String, content: String) =
push(content.byteInputStream(), System.currentTimeMillis(), 644, RemoteFile(targetFile)) push(content.byteInputStream(), System.currentTimeMillis(), 644, RemoteFile(targetFile))

View File

@@ -1,40 +1,40 @@
package app.revanced.utils.adb package app.revanced.utils.adb
internal object Constants { internal object Constants {
internal const val PLACEHOLDER = "TEMPLATE_PACKAGE_NAME" internal const val PLACEHOLDER = "PLACEHOLDER"
internal const val PATH_INIT_PUSH = "/data/local/tmp/revanced.delete" internal const val TMP_PATH = "/data/local/tmp/revanced.tmp"
internal const val PATH_INSTALLATION = "/data/adb/revanced/" internal const val INSTALLATION_PATH = "/data/adb/revanced/"
internal const val PATH_PATCHED_APK = "$PATH_INSTALLATION$PLACEHOLDER.apk" internal const val PATCHED_APK_PATH = "$INSTALLATION_PATH$PLACEHOLDER.apk"
internal const val PATH_MOUNT = "/data/adb/service.d/mount_revanced_$PLACEHOLDER.sh" internal const val MOUNT_PATH = "/data/adb/service.d/mount_revanced_$PLACEHOLDER.sh"
internal const val COMMAND_DELETE = "rm -rf $PLACEHOLDER" internal const val DELETE = "rm -rf $PLACEHOLDER"
internal const val COMMAND_CREATE_DIR = "mkdir -p" internal const val CREATE_DIR = "mkdir -p"
internal const val COMMAND_RESTART = "pm resolve-activity --brief $PLACEHOLDER | tail -n 1 | " + internal const val RESTART = "pm resolve-activity --brief $PLACEHOLDER | tail -n 1 | " +
"xargs am start -n && kill ${'$'}(pidof -s $PLACEHOLDER)" "xargs am start -n && kill ${'$'}(pidof -s $PLACEHOLDER)"
internal const val COMMAND_PREPARE_MOUNT_APK = "base_path=\"$PATH_PATCHED_APK\" && " + internal const val INSTALL_PATCHED_APK = "base_path=\"$PATCHED_APK_PATH\" && " +
"mv $PATH_INIT_PUSH ${'$'}base_path && " + "mv $TMP_PATH ${'$'}base_path && " +
"chmod 644 ${'$'}base_path && " + "chmod 644 ${'$'}base_path && " +
"chown system:system ${'$'}base_path && " + "chown system:system ${'$'}base_path && " +
"chcon u:object_r:apk_data_file:s0 ${'$'}base_path" "chcon u:object_r:apk_data_file:s0 ${'$'}base_path"
internal const val COMMAND_UMOUNT = internal const val 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" "grep $PLACEHOLDER /proc/mounts | while read -r line; do echo ${'$'}line | cut -d \" \" -f 2 | sed 's/apk.*/apk/' | xargs -r umount -l; done"
internal const val COMMAND_INSTALL_MOUNT = "mv $PATH_INIT_PUSH $PATH_MOUNT && chmod +x $PATH_MOUNT" internal const val INSTALL_MOUNT = "mv $TMP_PATH $MOUNT_PATH && chmod +x $MOUNT_PATH"
internal const val CONTENT_MOUNT_SCRIPT = internal val MOUNT_SCRIPT =
""" """
#!/system/bin/sh #!/system/bin/sh
MAGISKTMP="${'$'}(magisk --path)" || MAGISKTMP=/sbin MAGISKTMP="${'$'}(magisk --path)" || MAGISKTMP=/sbin
MIRROR="${'$'}MAGISKTMP/.magisk/mirror" MIRROR="${'$'}MAGISKTMP/.magisk/mirror"
while [ "${'$'}(getprop sys.boot_completed | tr -d '\r')" != "1" ]; do sleep 1; done while [ "${'$'}(getprop sys.boot_completed | tr -d '\r')" != "1" ]; do sleep 1; done
base_path="$PATH_PATCHED_APK" base_path="$PATCHED_APK_PATH"
stock_path=${'$'}( pm path $PLACEHOLDER | grep base | sed 's/package://g' ) stock_path=${'$'}( pm path $PLACEHOLDER | grep base | sed 's/package://g' )
chcon u:object_r:apk_data_file:s0 ${'$'}base_path chcon u:object_r:apk_data_file:s0 ${'$'}base_path
mount -o bind ${'$'}MIRROR${'$'}base_path ${'$'}stock_path mount -o bind ${'$'}MIRROR${'$'}base_path ${'$'}stock_path
""" """.trimIndent()
} }

View File

@@ -1,6 +1,5 @@
package app.revanced.utils.signing package app.revanced.utils.signing
import app.revanced.cli.command.logger
import com.android.apksig.ApkSigner import com.android.apksig.ApkSigner
import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x500.X500Name
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
@@ -16,10 +15,13 @@ import java.math.BigInteger
import java.security.* import java.security.*
import java.security.cert.X509Certificate import java.security.cert.X509Certificate
import java.util.* import java.util.*
import java.util.logging.Logger
internal class ApkSigner( internal class ApkSigner(
private val signingOptions: SigningOptions private val signingOptions: SigningOptions
) { ) {
private val logger = Logger.getLogger(ApkSigner::class.java.name)
private val signer: ApkSigner.Builder private val signer: ApkSigner.Builder
private val passwordCharArray = signingOptions.password.toCharArray() private val passwordCharArray = signingOptions.password.toCharArray()