mirror of
https://github.com/revanced/revanced-cli.git
synced 2025-01-07 09:45:50 +01:00
Merge pull request #3 from ReVancedTeam/picocli
refactor: migrate to `picocli`
This commit is contained in:
commit
d72d04d24f
@ -24,31 +24,25 @@ val patchesDependency = "app.revanced:revanced-patches:1.0.0-dev.4"
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(kotlin("stdlib"))
|
implementation(kotlin("stdlib"))
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-cli:0.3.4")
|
implementation("app.revanced:revanced-patcher:+")
|
||||||
|
|
||||||
implementation("app.revanced:revanced-patcher:1.0.0-dev.8")
|
|
||||||
implementation(patchesDependency)
|
implementation(patchesDependency)
|
||||||
|
implementation("info.picocli:picocli:+")
|
||||||
|
|
||||||
implementation("com.google.code.gson:gson:2.9.0")
|
implementation("me.tongfei:progressbar:+")
|
||||||
implementation("me.tongfei:progressbar:0.9.3")
|
|
||||||
implementation("com.github.li-wjohnson:jadb:master-SNAPSHOT") // using a fork instead.
|
implementation("com.github.li-wjohnson:jadb:master-SNAPSHOT") // using a fork instead.
|
||||||
implementation("org.bouncycastle:bcpkix-jdk15on:1.70")
|
implementation("org.bouncycastle:bcpkix-jdk15on:+")
|
||||||
}
|
}
|
||||||
|
|
||||||
val cliMainClass = "app.revanced.cli.Main"
|
|
||||||
|
|
||||||
tasks {
|
tasks {
|
||||||
build {
|
build {
|
||||||
dependsOn(shadowJar)
|
dependsOn(shadowJar)
|
||||||
}
|
}
|
||||||
shadowJar {
|
shadowJar {
|
||||||
dependencies {
|
dependencies {
|
||||||
// This makes sure we link to the library, but don't include it.
|
|
||||||
// So, a "runtime only" dependency.
|
|
||||||
exclude(dependency(patchesDependency))
|
exclude(dependency(patchesDependency))
|
||||||
}
|
}
|
||||||
manifest {
|
manifest {
|
||||||
attributes("Main-Class" to cliMainClass)
|
attributes("Main-Class" to "app.revanced.cli.MainKt")
|
||||||
attributes("Implementation-Title" to project.name)
|
attributes("Implementation-Title" to project.name)
|
||||||
attributes("Implementation-Version" to project.version)
|
attributes("Implementation-Version" to project.version)
|
||||||
}
|
}
|
||||||
|
@ -1,178 +1,7 @@
|
|||||||
package app.revanced.cli
|
package app.revanced.cli
|
||||||
|
|
||||||
import app.revanced.cli.runner.AdbRunner
|
import picocli.CommandLine
|
||||||
import app.revanced.cli.utils.PatchLoader
|
|
||||||
import app.revanced.cli.utils.Patches
|
|
||||||
import app.revanced.cli.utils.Preconditions
|
|
||||||
import app.revanced.patcher.Patcher
|
|
||||||
import app.revanced.patcher.patch.PatchMetadata
|
|
||||||
import app.revanced.patcher.patch.PatchResult
|
|
||||||
import kotlinx.cli.ArgParser
|
|
||||||
import kotlinx.cli.ArgType
|
|
||||||
import kotlinx.cli.default
|
|
||||||
import kotlinx.cli.required
|
|
||||||
import me.tongfei.progressbar.ProgressBarBuilder
|
|
||||||
import me.tongfei.progressbar.ProgressBarStyle
|
|
||||||
import java.io.File
|
|
||||||
import java.nio.file.Files
|
|
||||||
|
|
||||||
private const val CLI_NAME = "ReVanced CLI"
|
internal fun main(args: Array<String>) {
|
||||||
private val CLI_VERSION = Main::class.java.`package`.implementationVersion ?: "0.0.0-unknown"
|
CommandLine(MainCommand).execute(*args)
|
||||||
|
|
||||||
class Main {
|
|
||||||
companion object {
|
|
||||||
private fun runCLI(
|
|
||||||
inApk: String,
|
|
||||||
inPatches: String,
|
|
||||||
inIntegrations: String?,
|
|
||||||
inOutput: String,
|
|
||||||
inRunOnAdb: String?,
|
|
||||||
hideResults: Boolean,
|
|
||||||
noLogging: Boolean,
|
|
||||||
) {
|
|
||||||
val bar = ProgressBarBuilder()
|
|
||||||
.setTaskName("Working..")
|
|
||||||
.setUpdateIntervalMillis(25)
|
|
||||||
.continuousUpdate()
|
|
||||||
.setStyle(ProgressBarStyle.ASCII)
|
|
||||||
.build()
|
|
||||||
.maxHint(1)
|
|
||||||
.setExtraMessage("Initializing")
|
|
||||||
val apk = Preconditions.isFile(inApk)
|
|
||||||
val patchesFile = Preconditions.isFile(inPatches)
|
|
||||||
val output = Preconditions.isDirectory(inOutput)
|
|
||||||
bar.step()
|
|
||||||
|
|
||||||
val patcher = Patcher(apk)
|
|
||||||
|
|
||||||
inIntegrations?.let {
|
|
||||||
bar.reset().maxHint(1)
|
|
||||||
.extraMessage = "Merging integrations"
|
|
||||||
val integrations = Preconditions.isFile(it)
|
|
||||||
patcher.addFiles(listOf(integrations))
|
|
||||||
bar.step()
|
|
||||||
}
|
|
||||||
|
|
||||||
bar.reset().maxHint(1)
|
|
||||||
.extraMessage = "Loading patches"
|
|
||||||
PatchLoader.injectPatches(patchesFile)
|
|
||||||
val patches = Patches.loadPatches().map { it() }
|
|
||||||
patcher.addPatches(patches)
|
|
||||||
bar.step()
|
|
||||||
|
|
||||||
bar.reset().maxHint(1)
|
|
||||||
.extraMessage = "Resolving signatures"
|
|
||||||
patcher.resolveSignatures()
|
|
||||||
bar.step()
|
|
||||||
|
|
||||||
val szPatches = patches.size.toLong()
|
|
||||||
bar.reset().maxHint(szPatches)
|
|
||||||
.extraMessage = "Applying patches"
|
|
||||||
val results = patcher.applyPatches {
|
|
||||||
bar.step().extraMessage = "Applying $it"
|
|
||||||
}
|
|
||||||
|
|
||||||
bar.reset().maxHint(-1)
|
|
||||||
.extraMessage = "Generating dex files"
|
|
||||||
val dexFiles = patcher.save()
|
|
||||||
|
|
||||||
val szDexFiles = dexFiles.size.toLong()
|
|
||||||
bar.reset().maxHint(szDexFiles)
|
|
||||||
.extraMessage = "Saving dex files"
|
|
||||||
dexFiles.forEach { (dexName, dexData) ->
|
|
||||||
Files.write(File(output, dexName).toPath(), dexData.data)
|
|
||||||
bar.step()
|
|
||||||
}
|
|
||||||
bar.stepTo(szDexFiles)
|
|
||||||
|
|
||||||
bar.close()
|
|
||||||
|
|
||||||
inRunOnAdb?.let { device ->
|
|
||||||
AdbRunner.runApk(
|
|
||||||
apk,
|
|
||||||
dexFiles,
|
|
||||||
output,
|
|
||||||
device,
|
|
||||||
noLogging
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
println("All done!")
|
|
||||||
if (!hideResults) {
|
|
||||||
printResults(results)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun printResults(results: Map<PatchMetadata, Result<PatchResult>>) {
|
|
||||||
for ((metadata, result) in results) {
|
|
||||||
if (result.isSuccess) {
|
|
||||||
println("${metadata.shortName} was applied successfully!")
|
|
||||||
} else {
|
|
||||||
println("${metadata.shortName} failed to apply! Cause:")
|
|
||||||
result.exceptionOrNull()!!.printStackTrace()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun main(args: Array<String>) {
|
|
||||||
println("$CLI_NAME version $CLI_VERSION")
|
|
||||||
val parser = ArgParser(CLI_NAME)
|
|
||||||
|
|
||||||
// TODO: add some kind of incremental building, so merging integrations can be skipped.
|
|
||||||
// this can be achieved manually, but doing it automatically is better.
|
|
||||||
|
|
||||||
val apk by parser.option(
|
|
||||||
ArgType.String,
|
|
||||||
fullName = "apk",
|
|
||||||
shortName = "a",
|
|
||||||
description = "APK file"
|
|
||||||
).required()
|
|
||||||
val patches by parser.option(
|
|
||||||
ArgType.String,
|
|
||||||
fullName = "patches",
|
|
||||||
shortName = "p",
|
|
||||||
description = "Patches JAR file"
|
|
||||||
).required()
|
|
||||||
val integrations by parser.option(
|
|
||||||
ArgType.String,
|
|
||||||
fullName = "integrations",
|
|
||||||
shortName = "i",
|
|
||||||
description = "Integrations APK file"
|
|
||||||
)
|
|
||||||
val output by parser.option(
|
|
||||||
ArgType.String,
|
|
||||||
fullName = "output",
|
|
||||||
shortName = "o",
|
|
||||||
description = "Output directory"
|
|
||||||
).required()
|
|
||||||
val runOnAdb by parser.option(
|
|
||||||
ArgType.String,
|
|
||||||
fullName = "run-on",
|
|
||||||
description = "After the CLI is done building, which ADB device should it run on?"
|
|
||||||
)
|
|
||||||
// TODO: package name
|
|
||||||
val hideResults by parser.option(
|
|
||||||
ArgType.Boolean,
|
|
||||||
fullName = "hide-results",
|
|
||||||
description = "Don't print the patch results."
|
|
||||||
).default(false)
|
|
||||||
val noLogging by parser.option(
|
|
||||||
ArgType.Boolean,
|
|
||||||
fullName = "no-logging",
|
|
||||||
description = "Don't print the output of the application when used in combination with \"run-on\"."
|
|
||||||
).default(false)
|
|
||||||
|
|
||||||
parser.parse(args)
|
|
||||||
runCLI(
|
|
||||||
apk,
|
|
||||||
patches,
|
|
||||||
integrations,
|
|
||||||
output,
|
|
||||||
runOnAdb,
|
|
||||||
hideResults,
|
|
||||||
noLogging,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
84
src/main/kotlin/app/revanced/cli/MainCommand.kt
Normal file
84
src/main/kotlin/app/revanced/cli/MainCommand.kt
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
package app.revanced.cli
|
||||||
|
|
||||||
|
import app.revanced.patch.PatchLoader
|
||||||
|
import app.revanced.patch.Patches
|
||||||
|
import app.revanced.utils.adb.Adb
|
||||||
|
import picocli.CommandLine.*
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
@Command(
|
||||||
|
name = "ReVanced-CLI", version = ["1.0.0"], mixinStandardHelpOptions = true
|
||||||
|
)
|
||||||
|
internal object MainCommand : Runnable {
|
||||||
|
@Parameters(
|
||||||
|
paramLabel = "INCLUDE",
|
||||||
|
description = ["Which patches to include. If none is specified, all compatible patches will be included"]
|
||||||
|
)
|
||||||
|
internal var includedPatches = arrayOf<String>()
|
||||||
|
|
||||||
|
@Option(names = ["-p", "--patches"], description = ["One or more bundles of patches"])
|
||||||
|
internal var patchBundles = arrayOf<File>()
|
||||||
|
|
||||||
|
@Option(names = ["-t", "--temp-dir"], description = ["Temporal resource cache directory"], required = true)
|
||||||
|
internal lateinit var cacheDirectory: String
|
||||||
|
|
||||||
|
@Option(names = ["-r", "--resource-patcher"], description = ["Enable patching resources"])
|
||||||
|
internal var patchResources: Boolean = false
|
||||||
|
|
||||||
|
@Option(
|
||||||
|
names = ["-c", "--clean"],
|
||||||
|
description = ["Clean the temporal resource cache directory. This will be done anyways when running the patcher"]
|
||||||
|
)
|
||||||
|
internal var clean: Boolean = false
|
||||||
|
|
||||||
|
@Option(names = ["-l", "--list"], description = ["List patches only"])
|
||||||
|
internal var listOnly: Boolean = false
|
||||||
|
|
||||||
|
@Option(names = ["-m", "--merge"], description = ["One or more dex file containers to merge"])
|
||||||
|
internal var mergeFiles = listOf<File>()
|
||||||
|
|
||||||
|
@Option(names = ["-a", "--apk"], description = ["Input file to be patched"], required = true)
|
||||||
|
internal lateinit var inputFile: File
|
||||||
|
|
||||||
|
@Option(names = ["-o", "--out"], description = ["Output file path"], required = true)
|
||||||
|
internal lateinit var outputPath: String
|
||||||
|
|
||||||
|
@Option(names = ["-d", "--deploy-on"], description = ["If specified, deploy to adb device with given name"])
|
||||||
|
internal var deploy: String? = null
|
||||||
|
|
||||||
|
override fun run() {
|
||||||
|
if (listOnly) {
|
||||||
|
patchBundles.forEach {
|
||||||
|
PatchLoader.injectPatches(it)
|
||||||
|
Patches.loadPatches().forEach {
|
||||||
|
println(it().metadata)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val patcher = app.revanced.patcher.Patcher(
|
||||||
|
inputFile,
|
||||||
|
cacheDirectory,
|
||||||
|
patchResources
|
||||||
|
)
|
||||||
|
|
||||||
|
Patcher.start(patcher)
|
||||||
|
|
||||||
|
if (clean) {
|
||||||
|
File(cacheDirectory).deleteRecursively()
|
||||||
|
}
|
||||||
|
|
||||||
|
val outputFile = File(outputPath)
|
||||||
|
|
||||||
|
deploy?.let {
|
||||||
|
Adb(
|
||||||
|
outputFile,
|
||||||
|
patcher.packageName,
|
||||||
|
deploy!!
|
||||||
|
).deploy()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clean) outputFile.delete()
|
||||||
|
}
|
||||||
|
}
|
91
src/main/kotlin/app/revanced/cli/Patcher.kt
Normal file
91
src/main/kotlin/app/revanced/cli/Patcher.kt
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
package app.revanced.cli
|
||||||
|
|
||||||
|
import app.revanced.patch.PatchLoader
|
||||||
|
import app.revanced.patch.Patches
|
||||||
|
import app.revanced.patcher.data.base.Data
|
||||||
|
import app.revanced.patcher.patch.base.Patch
|
||||||
|
import app.revanced.utils.filesystem.FileSystemUtils
|
||||||
|
import app.revanced.utils.signing.Signer
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
internal class Patcher {
|
||||||
|
internal companion object {
|
||||||
|
internal fun start(patcher: app.revanced.patcher.Patcher) {
|
||||||
|
// merge files like necessary integrations
|
||||||
|
patcher.addFiles(MainCommand.mergeFiles)
|
||||||
|
// add patches, but filter incompatible or excluded patches
|
||||||
|
patcher.addPatchesFiltered()
|
||||||
|
// apply patches
|
||||||
|
for ((meta, result) in patcher.applyPatches {
|
||||||
|
println("Applying $it.")
|
||||||
|
}) {
|
||||||
|
println("Applied ${meta.name}. The result was $result.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// write output file
|
||||||
|
val outFile = File(MainCommand.outputPath)
|
||||||
|
if (outFile.exists()) outFile.delete()
|
||||||
|
MainCommand.inputFile.copyTo(outFile)
|
||||||
|
|
||||||
|
val zipFileSystem = FileSystemUtils(outFile)
|
||||||
|
|
||||||
|
// replace all dex files
|
||||||
|
for ((name, data) in patcher.save()) {
|
||||||
|
zipFileSystem.replaceFile(name, data.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (MainCommand.patchResources) {
|
||||||
|
for (file in File(MainCommand.cacheDirectory).resolve("build/").listFiles().first().listFiles()) {
|
||||||
|
if (!file.isDirectory) {
|
||||||
|
zipFileSystem.replaceFile(file.name, file.readBytes())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
zipFileSystem.replaceDirectory(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// finally close the stream
|
||||||
|
zipFileSystem.close()
|
||||||
|
|
||||||
|
// and sign the apk file
|
||||||
|
Signer.signApk(outFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun app.revanced.patcher.Patcher.addPatchesFiltered() {
|
||||||
|
// TODO: get package metadata (outside of this method) for apk file which needs to be patched
|
||||||
|
val packageName = this.packageName
|
||||||
|
val packageVersion = this.packageVersion
|
||||||
|
|
||||||
|
val checkInclude = MainCommand.includedPatches.isNotEmpty()
|
||||||
|
|
||||||
|
MainCommand.patchBundles.forEach { bundle ->
|
||||||
|
PatchLoader.injectPatches(bundle)
|
||||||
|
val includedPatches = mutableListOf<Patch<Data>>()
|
||||||
|
Patches.loadPatches().forEach patch@{
|
||||||
|
val patch = it()
|
||||||
|
|
||||||
|
// TODO: filter out incompatible patches with package metadata
|
||||||
|
val filterOutPatches = true
|
||||||
|
if (filterOutPatches && !patch.metadata.compatiblePackages.any { packageMetadata ->
|
||||||
|
packageMetadata.name == packageName && packageMetadata.versions.any {
|
||||||
|
it == packageVersion
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
|
||||||
|
println("Skipping ${patch.metadata.name} due to incompatibility with current package $packageName.")
|
||||||
|
return@patch
|
||||||
|
}
|
||||||
|
|
||||||
|
if (checkInclude && !MainCommand.includedPatches.contains(patch.metadata.shortName)) {
|
||||||
|
return@patch
|
||||||
|
}
|
||||||
|
|
||||||
|
println("Adding ${patch.metadata.name}.")
|
||||||
|
includedPatches.add(patch)
|
||||||
|
|
||||||
|
}
|
||||||
|
this.addPatches(includedPatches)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,160 +0,0 @@
|
|||||||
package app.revanced.cli.runner
|
|
||||||
|
|
||||||
import app.revanced.cli.utils.DexReplacer
|
|
||||||
import app.revanced.cli.utils.Scripts
|
|
||||||
import app.revanced.cli.utils.signer.Signer
|
|
||||||
import me.tongfei.progressbar.ProgressBar
|
|
||||||
import me.tongfei.progressbar.ProgressBarBuilder
|
|
||||||
import me.tongfei.progressbar.ProgressBarStyle
|
|
||||||
import org.jf.dexlib2.writer.io.MemoryDataStore
|
|
||||||
import se.vidstige.jadb.JadbConnection
|
|
||||||
import se.vidstige.jadb.JadbDevice
|
|
||||||
import se.vidstige.jadb.RemoteFile
|
|
||||||
import se.vidstige.jadb.ShellProcessBuilder
|
|
||||||
import java.io.File
|
|
||||||
import java.util.concurrent.Executors
|
|
||||||
|
|
||||||
object AdbRunner {
|
|
||||||
fun runApk(
|
|
||||||
apk: File,
|
|
||||||
dexFiles: Map<String, MemoryDataStore>,
|
|
||||||
outputDir: File,
|
|
||||||
deviceName: String,
|
|
||||||
noLogging: Boolean
|
|
||||||
) {
|
|
||||||
lateinit var dvc: JadbDevice
|
|
||||||
pbar("Initializing").use { bar ->
|
|
||||||
dvc = JadbConnection().findDevice(deviceName)
|
|
||||||
?: throw IllegalArgumentException("No such device with name $deviceName")
|
|
||||||
if (!dvc.hasSu())
|
|
||||||
throw IllegalArgumentException("Device $deviceName is not rooted or does not have su")
|
|
||||||
bar.step()
|
|
||||||
}
|
|
||||||
|
|
||||||
lateinit var tmpFile: File // we need this file at the end to clean up.
|
|
||||||
pbar("Generating APK file", 3).use { bar ->
|
|
||||||
bar.step().extraMessage = "Creating APK file"
|
|
||||||
tmpFile = File(outputDir, "revanced.apk")
|
|
||||||
apk.copyTo(tmpFile, true)
|
|
||||||
|
|
||||||
bar.step().extraMessage = "Replacing dex files"
|
|
||||||
DexReplacer.replaceDex(tmpFile, dexFiles)
|
|
||||||
|
|
||||||
bar.step().extraMessage = "Signing APK file"
|
|
||||||
try {
|
|
||||||
Signer.signApk(tmpFile)
|
|
||||||
} catch (e: SecurityException) {
|
|
||||||
throw IllegalStateException(
|
|
||||||
"A security exception occurred when signing the APK! " +
|
|
||||||
"If it has anything to with \"cannot authenticate\" then please make sure " +
|
|
||||||
"you are using Zulu or OpenJDK as they do work when using the adb runner.",
|
|
||||||
e
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pbar("Running application", 6, false).use { bar ->
|
|
||||||
bar.step().extraMessage = "Pushing mount scripts"
|
|
||||||
dvc.push(Scripts.MOUNT_SCRIPT, RemoteFile(Scripts.SCRIPT_PATH))
|
|
||||||
dvc.cmd(Scripts.CREATE_DIR_COMMAND).assertZero()
|
|
||||||
dvc.cmd(Scripts.MV_MOUNT_COMMAND).assertZero()
|
|
||||||
dvc.cmd(Scripts.CHMOD_MOUNT_COMMAND).assertZero()
|
|
||||||
|
|
||||||
bar.step().extraMessage = "Pushing APK file"
|
|
||||||
dvc.push(tmpFile, RemoteFile(Scripts.APK_PATH))
|
|
||||||
|
|
||||||
bar.step().extraMessage = "Mounting APK file"
|
|
||||||
dvc.cmd(Scripts.STOP_APP_COMMAND).startAndWait()
|
|
||||||
dvc.cmd(Scripts.START_MOUNT_COMMAND).assertZero()
|
|
||||||
|
|
||||||
bar.step().extraMessage = "Starting APK file"
|
|
||||||
dvc.cmd(Scripts.START_APP_COMMAND).assertZero()
|
|
||||||
|
|
||||||
bar.step().setExtraMessage("Debugging APK file").refresh()
|
|
||||||
println("\nWaiting until app is closed.")
|
|
||||||
val executor = Executors.newSingleThreadExecutor()
|
|
||||||
val pipe = if (noLogging) {
|
|
||||||
ProcessBuilder.Redirect.PIPE
|
|
||||||
} else {
|
|
||||||
ProcessBuilder.Redirect.INHERIT
|
|
||||||
}
|
|
||||||
val p = dvc.cmd(Scripts.LOGCAT_COMMAND)
|
|
||||||
.redirectOutput(pipe)
|
|
||||||
.redirectError(pipe)
|
|
||||||
.useExecutor(executor)
|
|
||||||
.start()
|
|
||||||
Thread.sleep(250) // give the app some time to start up.
|
|
||||||
while (true) {
|
|
||||||
try {
|
|
||||||
while (dvc.cmd(Scripts.PIDOF_APP_COMMAND).startAndWait() == 0) {
|
|
||||||
Thread.sleep(250)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
} catch (e: Exception) {
|
|
||||||
throw RuntimeException("An error occurred while monitoring state of app", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
println("App closed, continuing.")
|
|
||||||
p.destroy()
|
|
||||||
executor.shutdown()
|
|
||||||
|
|
||||||
bar.step().extraMessage = "Unmounting APK file"
|
|
||||||
var exitCode: Int
|
|
||||||
do {
|
|
||||||
exitCode = dvc.cmd(Scripts.UNMOUNT_COMMAND).startAndWait()
|
|
||||||
} while (exitCode != 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun JadbDevice.push(s: String, remoteFile: RemoteFile) =
|
|
||||||
this.push(s.byteInputStream(), System.currentTimeMillis(), 644, remoteFile)
|
|
||||||
|
|
||||||
private fun JadbConnection.findDevice(device: String): JadbDevice? {
|
|
||||||
return devices.find { it.serial == device }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun JadbDevice.cmd(s: String): ShellProcessBuilder {
|
|
||||||
val args = s.split(" ") as ArrayList<String>
|
|
||||||
val cmd = args.removeFirst()
|
|
||||||
return shellProcessBuilder(cmd, *args.toTypedArray())
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun JadbDevice.hasSu(): Boolean {
|
|
||||||
return cmd("su -h").startAndWait() == 0
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun ShellProcessBuilder.startAndWait(): Int {
|
|
||||||
return start().waitFor()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun ShellProcessBuilder.assertZero() {
|
|
||||||
if (startAndWait() != 0) {
|
|
||||||
val cmd = getcmd()
|
|
||||||
throw IllegalStateException("ADB returned non-zero status code for command: $cmd")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun pbar(task: String, steps: Long = 1, update: Boolean = true): ProgressBar {
|
|
||||||
val b = ProgressBarBuilder().setTaskName(task)
|
|
||||||
if (update) b
|
|
||||||
.setUpdateIntervalMillis(250)
|
|
||||||
.continuousUpdate()
|
|
||||||
return b
|
|
||||||
.setStyle(ProgressBarStyle.ASCII)
|
|
||||||
.build()
|
|
||||||
.maxHint(steps + 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun ProgressBar.use(block: (ProgressBar) -> Unit) {
|
|
||||||
block(this)
|
|
||||||
stepTo(max) // step to 100%
|
|
||||||
extraMessage = "" // clear extra message
|
|
||||||
close()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun ShellProcessBuilder.getcmd(): String {
|
|
||||||
val f = this::class.java.getDeclaredField("command")
|
|
||||||
f.isAccessible = true
|
|
||||||
return f.get(this) as String
|
|
||||||
}
|
|
@ -1,31 +0,0 @@
|
|||||||
package app.revanced.cli.utils
|
|
||||||
|
|
||||||
import lanchon.multidexlib2.BasicDexFileNamer
|
|
||||||
import org.jf.dexlib2.writer.io.MemoryDataStore
|
|
||||||
import java.io.File
|
|
||||||
import java.nio.file.FileSystems
|
|
||||||
import java.nio.file.Files
|
|
||||||
|
|
||||||
val NAMER = BasicDexFileNamer()
|
|
||||||
|
|
||||||
object DexReplacer {
|
|
||||||
fun replaceDex(source: File, dexFiles: Map<String, MemoryDataStore>) {
|
|
||||||
FileSystems.newFileSystem(
|
|
||||||
source.toPath(),
|
|
||||||
null
|
|
||||||
).use { fs ->
|
|
||||||
// Delete all classes?.dex files
|
|
||||||
Files.walk(fs.rootDirectories.first()).forEach {
|
|
||||||
if (
|
|
||||||
it.toString().endsWith(".dex") &&
|
|
||||||
NAMER.isValidName(it.fileName.toString())
|
|
||||||
) Files.delete(it)
|
|
||||||
}
|
|
||||||
// Write new dex files
|
|
||||||
dexFiles
|
|
||||||
.forEach { (dexName, dexData) ->
|
|
||||||
Files.write(fs.getPath("/$dexName"), dexData.data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,24 +0,0 @@
|
|||||||
package app.revanced.cli.utils
|
|
||||||
|
|
||||||
import java.io.File
|
|
||||||
import java.io.FileNotFoundException
|
|
||||||
|
|
||||||
class Preconditions {
|
|
||||||
companion object {
|
|
||||||
fun isFile(path: String): File {
|
|
||||||
val f = File(path)
|
|
||||||
if (!f.exists()) {
|
|
||||||
throw FileNotFoundException(f.toString())
|
|
||||||
}
|
|
||||||
return f
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isDirectory(path: String): File {
|
|
||||||
val f = isFile(path)
|
|
||||||
if (!f.isDirectory) {
|
|
||||||
throw IllegalArgumentException("$f is not a directory")
|
|
||||||
}
|
|
||||||
return f
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,34 +0,0 @@
|
|||||||
package app.revanced.cli.utils
|
|
||||||
|
|
||||||
// TODO: make this a class with PACKAGE_NAME as argument, then use that everywhere.
|
|
||||||
// make sure to remove the "const" from all the vals, they won't compile obviously.
|
|
||||||
object Scripts {
|
|
||||||
private const val PACKAGE_NAME = "com.google.android.youtube"
|
|
||||||
private const val DATA_PATH = "/data/adb/ReVanced"
|
|
||||||
const val APK_PATH = "/sdcard/base.apk"
|
|
||||||
const val SCRIPT_PATH = "/sdcard/mount.sh"
|
|
||||||
|
|
||||||
val MOUNT_SCRIPT =
|
|
||||||
"""
|
|
||||||
base_path="$DATA_PATH/base.apk"
|
|
||||||
stock_path=${'$'}{ pm path $PACKAGE_NAME | grep base | sed 's/package://g' }
|
|
||||||
umount -l ${'$'}stock_path
|
|
||||||
rm ${'$'}base_path
|
|
||||||
mv "$APK_PATH" ${'$'}base_path
|
|
||||||
chmod 644 ${'$'}base_path
|
|
||||||
chown system:system ${'$'}base_path
|
|
||||||
chcon u:object_r:apk_data_file:s0 ${'$'}base_path
|
|
||||||
mount -o bind ${'$'}base_path ${'$'}stock_path
|
|
||||||
""".trimIndent()
|
|
||||||
|
|
||||||
const val PIDOF_APP_COMMAND = "pidof -s $PACKAGE_NAME"
|
|
||||||
private const val PIDOF_APP = "\$($PIDOF_APP_COMMAND)"
|
|
||||||
const val CREATE_DIR_COMMAND = "su -c \"mkdir -p $DATA_PATH/\""
|
|
||||||
const val MV_MOUNT_COMMAND = "su -c \"mv /sdcard/mount.sh $DATA_PATH/\""
|
|
||||||
const val CHMOD_MOUNT_COMMAND = "su -c \"chmod +x $DATA_PATH/mount.sh\""
|
|
||||||
const val START_MOUNT_COMMAND = "su -c $DATA_PATH/mount.sh"
|
|
||||||
const val UNMOUNT_COMMAND = "su -c \"umount -l $(pm path $PACKAGE_NAME | grep base | sed 's/package://g')\""
|
|
||||||
const val LOGCAT_COMMAND = "su -c \"logcat -c && logcat --pid=$PIDOF_APP\""
|
|
||||||
const val STOP_APP_COMMAND = "su -c \"kill $PIDOF_APP\""
|
|
||||||
const val START_APP_COMMAND = "monkey -p $PACKAGE_NAME 1"
|
|
||||||
}
|
|
@ -1,12 +1,12 @@
|
|||||||
package app.revanced.cli.utils
|
package app.revanced.patch
|
||||||
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.net.URLClassLoader
|
import java.net.URLClassLoader
|
||||||
|
|
||||||
class PatchLoader {
|
internal class PatchLoader {
|
||||||
companion object {
|
internal companion object {
|
||||||
fun injectPatches(file: File) {
|
internal fun injectPatches(file: File) {
|
||||||
// This function will fail on Java 9 and above.
|
// This function will fail on Java 9 and above.
|
||||||
try {
|
try {
|
||||||
val url = file.toURI().toURL()
|
val url = file.toURI().toURL()
|
@ -1,15 +1,15 @@
|
|||||||
package app.revanced.cli.utils
|
package app.revanced.patch
|
||||||
|
|
||||||
import app.revanced.patches.Index
|
import app.revanced.patches.Index
|
||||||
|
|
||||||
class Patches {
|
internal class Patches {
|
||||||
companion object {
|
internal companion object {
|
||||||
// You may ask yourself, "why do this?".
|
// You may ask yourself, "why do this?".
|
||||||
// We do it like this, because we don't want the Index class
|
// We do it like this, because we don't want the Index class
|
||||||
// to be loaded while the dependency hasn't been injected yet.
|
// to be loaded while the dependency hasn't been injected yet.
|
||||||
// You can see this as "controlled class loading".
|
// You can see this as "controlled class loading".
|
||||||
// Whenever this class is loaded (because it is invoked), all the imports
|
// Whenever this class is loaded (because it is invoked), all the imports
|
||||||
// will be loaded too. We don't want to do this until we've injected the class.
|
// will be loaded too. We don't want to do this until we've injected the class.
|
||||||
fun loadPatches() = Index.patches
|
internal fun loadPatches() = Index.patches
|
||||||
}
|
}
|
||||||
}
|
}
|
94
src/main/kotlin/app/revanced/utils/adb/Adb.kt
Normal file
94
src/main/kotlin/app/revanced/utils/adb/Adb.kt
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
package app.revanced.utils.adb
|
||||||
|
|
||||||
|
import se.vidstige.jadb.JadbConnection
|
||||||
|
import se.vidstige.jadb.JadbDevice
|
||||||
|
import java.io.File
|
||||||
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
|
internal class Adb(
|
||||||
|
private val apk: File,
|
||||||
|
private val packageName: String,
|
||||||
|
deviceName: String,
|
||||||
|
private val logging: Boolean = true
|
||||||
|
) {
|
||||||
|
private val device: JadbDevice
|
||||||
|
|
||||||
|
init {
|
||||||
|
device = JadbConnection().devices.find { it.serial == deviceName }
|
||||||
|
?: throw IllegalArgumentException("No such device with name $deviceName")
|
||||||
|
|
||||||
|
if (device.run("su -h", false) != 0)
|
||||||
|
throw IllegalArgumentException("Root required on $deviceName. Deploying failed.")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun String.replacePlaceholder(): String {
|
||||||
|
return this.replace(Constants.PLACEHOLDER, packageName)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun deploy() {
|
||||||
|
// create revanced path
|
||||||
|
device.run("${Constants.COMMAND_CREATE_DIR} ${Constants.PATH_REVANCED}")
|
||||||
|
|
||||||
|
// push patched file
|
||||||
|
device.copy(Constants.PATH_INIT_PUSH, apk)
|
||||||
|
// install apk
|
||||||
|
device.run(Constants.COMMAND_INSTALL_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())
|
||||||
|
|
||||||
|
// push umount script
|
||||||
|
device.createFile(
|
||||||
|
Constants.PATH_INIT_PUSH,
|
||||||
|
Constants.CONTENT_UMOUNT_SCRIPT.replacePlaceholder()
|
||||||
|
)
|
||||||
|
// install mount script
|
||||||
|
device.run(Constants.COMMAND_INSTALL_UMOUNT.replacePlaceholder())
|
||||||
|
|
||||||
|
// unmount the apk for sanity
|
||||||
|
device.run(Constants.PATH_UMOUNT.replacePlaceholder())
|
||||||
|
// mount the apk
|
||||||
|
device.run(Constants.PATH_MOUNT.replacePlaceholder())
|
||||||
|
|
||||||
|
// relaunch app
|
||||||
|
device.run(Constants.COMMAND_RESTART.replacePlaceholder())
|
||||||
|
|
||||||
|
// log the app
|
||||||
|
log()
|
||||||
|
}
|
||||||
|
|
||||||
|
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 state of app", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println("App closed, continuing.")
|
||||||
|
process.destroy()
|
||||||
|
executor.shutdown()
|
||||||
|
}
|
||||||
|
}
|
29
src/main/kotlin/app/revanced/utils/adb/Commands.kt
Normal file
29
src/main/kotlin/app/revanced/utils/adb/Commands.kt
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
package app.revanced.utils.adb
|
||||||
|
|
||||||
|
import se.vidstige.jadb.JadbDevice
|
||||||
|
import se.vidstige.jadb.RemoteFile
|
||||||
|
import se.vidstige.jadb.ShellProcessBuilder
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
internal fun JadbDevice.buildCommand(command: String, su: Boolean = true): ShellProcessBuilder {
|
||||||
|
if (su) {
|
||||||
|
return shellProcessBuilder("su -c \'$command\'")
|
||||||
|
}
|
||||||
|
|
||||||
|
val args = command.split(" ") as ArrayList<String>
|
||||||
|
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.copy(targetPath: String, file: File) {
|
||||||
|
push(file, RemoteFile(targetPath))
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun JadbDevice.createFile(targetFile: String, content: String) {
|
||||||
|
push(content.byteInputStream(), System.currentTimeMillis(), 644, RemoteFile(targetFile))
|
||||||
|
}
|
57
src/main/kotlin/app/revanced/utils/adb/Constants.kt
Normal file
57
src/main/kotlin/app/revanced/utils/adb/Constants.kt
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
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 --pid=$($COMMAND_PID_OF $PLACEHOLDER)"
|
||||||
|
internal const val COMMAND_RESTART = "monkey -p $PLACEHOLDER 1 && kill ${'$'}($COMMAND_PID_OF $PLACEHOLDER)"
|
||||||
|
|
||||||
|
// default mount file name
|
||||||
|
private const val NAME_MOUNT_SCRIPT = "mount_$PLACEHOLDER.sh"
|
||||||
|
|
||||||
|
// initial directory to push files to via adb push
|
||||||
|
internal const val PATH_INIT_PUSH = "/sdcard/revanced.delete"
|
||||||
|
|
||||||
|
// revanced path
|
||||||
|
internal const val PATH_REVANCED = "/data/adb/revanced/"
|
||||||
|
|
||||||
|
// revanced apk path
|
||||||
|
private const val PATH_REVANCED_APP = "$PATH_REVANCED$PLACEHOLDER.apk"
|
||||||
|
|
||||||
|
// (un)mount script paths
|
||||||
|
internal const val PATH_MOUNT = "/data/adb/service.d/$NAME_MOUNT_SCRIPT"
|
||||||
|
internal const val PATH_UMOUNT = "/data/adb/post-fs-data.d/un$NAME_MOUNT_SCRIPT"
|
||||||
|
|
||||||
|
// move to revanced apk path & set permissions
|
||||||
|
internal const val COMMAND_INSTALL_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"
|
||||||
|
|
||||||
|
// install mount script & set permissions
|
||||||
|
internal const val COMMAND_INSTALL_MOUNT = "mv $PATH_INIT_PUSH $PATH_MOUNT && $COMMAND_CHMOD_MOUNT $PATH_MOUNT"
|
||||||
|
|
||||||
|
// install umount script & set permissions
|
||||||
|
internal const val COMMAND_INSTALL_UMOUNT = "mv $PATH_INIT_PUSH $PATH_UMOUNT && $COMMAND_CHMOD_MOUNT $PATH_UMOUNT"
|
||||||
|
|
||||||
|
// unmount script
|
||||||
|
internal val CONTENT_UMOUNT_SCRIPT =
|
||||||
|
"""
|
||||||
|
#!/system/bin/sh
|
||||||
|
while read line; do echo ${'$'}{line} | grep $PLACEHOLDER | awk '{print ${'$'}2}' | xargs umount -l; done< /proc/mounts
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
// mount script
|
||||||
|
internal val CONTENT_MOUNT_SCRIPT =
|
||||||
|
"""
|
||||||
|
#!/system/bin/sh
|
||||||
|
while [ "${'$'}(getprop sys.boot_completed | tr -d '\r')" != "1" ]; do sleep 1; done
|
||||||
|
|
||||||
|
base_path="$PATH_REVANCED_APP"
|
||||||
|
stock_path=${'$'}{ pm path $PLACEHOLDER | grep base | sed 's/package://g' }
|
||||||
|
mount -o bind ${'$'}base_path ${'$'}stock_path
|
||||||
|
""".trimIndent()
|
||||||
|
}
|
@ -0,0 +1,66 @@
|
|||||||
|
package app.revanced.utils.filesystem
|
||||||
|
|
||||||
|
import java.io.Closeable
|
||||||
|
import java.io.File
|
||||||
|
import java.nio.file.FileSystem
|
||||||
|
import java.nio.file.FileSystems
|
||||||
|
import java.nio.file.Files
|
||||||
|
|
||||||
|
internal class FileSystemUtils(
|
||||||
|
file: File
|
||||||
|
) : Closeable {
|
||||||
|
private var fileSystem: FileSystem
|
||||||
|
|
||||||
|
init {
|
||||||
|
fileSystem = FileSystems.newFileSystem(
|
||||||
|
file.toPath(),
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun deleteDirectory(dirPath: String) {
|
||||||
|
val files = Files.walk(fileSystem.getPath("$dirPath/"))
|
||||||
|
|
||||||
|
files
|
||||||
|
.sorted(Comparator.reverseOrder())
|
||||||
|
.forEach {
|
||||||
|
|
||||||
|
Files.delete(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
files.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
internal fun replaceDirectory(replacement: File) {
|
||||||
|
if (!replacement.isDirectory) throw Exception("${replacement.name} is not a directory.")
|
||||||
|
|
||||||
|
// FIXME: make this delete the directory recursively
|
||||||
|
//deleteDirectory(replacement.name)
|
||||||
|
//val path = Files.createDirectory(fileSystem.getPath(replacement.name))
|
||||||
|
|
||||||
|
val excludeFromPath = replacement.path.removeSuffix(replacement.name)
|
||||||
|
for (path in Files.walk(replacement.toPath())) {
|
||||||
|
val file = path.toFile()
|
||||||
|
if (file.isDirectory) {
|
||||||
|
val relativePath = path.toString().removePrefix(excludeFromPath)
|
||||||
|
val fileSystemPath = fileSystem.getPath(relativePath)
|
||||||
|
if (!Files.exists(fileSystemPath)) Files.createDirectory(fileSystemPath)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
replaceFile(path.toString().removePrefix(excludeFromPath), file.readBytes())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun replaceFile(sourceFile: String, content: ByteArray) {
|
||||||
|
val path = fileSystem.getPath(sourceFile)
|
||||||
|
Files.deleteIfExists(path)
|
||||||
|
Files.write(path, content)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
fileSystem.close()
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package app.revanced.cli.utils.signer
|
package app.revanced.utils.signing
|
||||||
|
|
||||||
import java.security.PrivateKey
|
import java.security.PrivateKey
|
||||||
import java.security.cert.X509Certificate
|
import java.security.cert.X509Certificate
|
@ -3,7 +3,7 @@
|
|||||||
* Licensed under the Open Software License version 3.0
|
* Licensed under the Open Software License version 3.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package app.revanced.cli.utils.signer
|
package app.revanced.utils.signing
|
||||||
|
|
||||||
import org.bouncycastle.asn1.x500.X500Name
|
import org.bouncycastle.asn1.x500.X500Name
|
||||||
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
|
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
|
||||||
@ -41,7 +41,7 @@ val PASSWORD = "revanced".toCharArray() // TODO: make it secure; random password
|
|||||||
/**
|
/**
|
||||||
* APK Signer.
|
* APK Signer.
|
||||||
* @author Aliucord authors
|
* @author Aliucord authors
|
||||||
* @author ReVanced Team
|
* @author ReVanced team
|
||||||
*/
|
*/
|
||||||
object Signer {
|
object Signer {
|
||||||
private fun newKeystore(out: File) {
|
private fun newKeystore(out: File) {
|
Loading…
Reference in New Issue
Block a user