mirror of
https://github.com/ReVanced/revanced-library.git
synced 2026-01-20 18:03:57 +00:00
feat: Add local Android installer (#25)
This commit is contained in:
369
src/commonMain/kotlin/app/revanced/library/ApkSigner.kt
Normal file
369
src/commonMain/kotlin/app/revanced/library/ApkSigner.kt
Normal file
@@ -0,0 +1,369 @@
|
||||
package app.revanced.library
|
||||
|
||||
import com.android.apksig.ApkSigner.SignerConfig
|
||||
import com.android.tools.build.apkzlib.sign.SigningExtension
|
||||
import com.android.tools.build.apkzlib.sign.SigningOptions
|
||||
import com.android.tools.build.apkzlib.zip.ZFile
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
|
||||
import org.bouncycastle.cert.X509v3CertificateBuilder
|
||||
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider
|
||||
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.math.BigInteger
|
||||
import java.security.*
|
||||
import java.security.cert.X509Certificate
|
||||
import java.util.*
|
||||
import java.util.logging.Logger
|
||||
|
||||
/**
|
||||
* Utility class for reading or writing keystore files and entries as well as signing APK files.
|
||||
*/
|
||||
@Suppress("MemberVisibilityCanBePrivate", "unused")
|
||||
object ApkSigner {
|
||||
private val logger = Logger.getLogger(ApkSigner::class.java.name)
|
||||
|
||||
init {
|
||||
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
|
||||
Security.addProvider(BouncyCastleProvider())
|
||||
}
|
||||
}
|
||||
|
||||
private fun newKeyStoreInstance() = KeyStore.getInstance("BKS", BouncyCastleProvider.PROVIDER_NAME)
|
||||
|
||||
/**
|
||||
* Create a new keystore with a new keypair.
|
||||
*
|
||||
* @param entries The entries to add to the keystore.
|
||||
*
|
||||
* @return The created keystore.
|
||||
*
|
||||
* @see KeyStoreEntry
|
||||
* @see KeyStore
|
||||
*/
|
||||
fun newKeyStore(entries: Set<KeyStoreEntry>): KeyStore {
|
||||
logger.fine("Creating keystore")
|
||||
|
||||
return newKeyStoreInstance().apply {
|
||||
load(null)
|
||||
|
||||
entries.forEach { entry ->
|
||||
// Add all entries to the keystore.
|
||||
setKeyEntry(
|
||||
entry.alias,
|
||||
entry.privateKeyCertificatePair.privateKey,
|
||||
entry.password.toCharArray(),
|
||||
arrayOf(entry.privateKeyCertificatePair.certificate),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a keystore from the given [keyStoreInputStream].
|
||||
*
|
||||
* @param keyStoreInputStream The stream to read the keystore from.
|
||||
* @param keyStorePassword The password for the keystore.
|
||||
*
|
||||
* @return The keystore.
|
||||
*
|
||||
* @throws IllegalArgumentException If the keystore password is invalid.
|
||||
*
|
||||
* @see KeyStore
|
||||
*/
|
||||
fun readKeyStore(
|
||||
keyStoreInputStream: InputStream,
|
||||
keyStorePassword: String?,
|
||||
): KeyStore {
|
||||
logger.fine("Reading keystore")
|
||||
|
||||
return newKeyStoreInstance().apply {
|
||||
try {
|
||||
load(keyStoreInputStream, keyStorePassword?.toCharArray())
|
||||
} catch (exception: IOException) {
|
||||
if (exception.cause is UnrecoverableKeyException) {
|
||||
throw IllegalArgumentException("Invalid keystore password")
|
||||
} else {
|
||||
throw exception
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new private key and certificate pair.
|
||||
*
|
||||
* @param commonName The common name of the certificate.
|
||||
* @param validUntil The date until which the certificate is valid.
|
||||
*
|
||||
* @return The newly created private key and certificate pair.
|
||||
*
|
||||
* @see PrivateKeyCertificatePair
|
||||
*/
|
||||
fun newPrivateKeyCertificatePair(
|
||||
commonName: String,
|
||||
validUntil: Date,
|
||||
): PrivateKeyCertificatePair {
|
||||
logger.fine("Creating certificate for $commonName")
|
||||
|
||||
// Generate a new key pair.
|
||||
val keyPair = KeyPairGenerator.getInstance("RSA").apply {
|
||||
initialize(4096)
|
||||
}.generateKeyPair()
|
||||
|
||||
val contentSigner = JcaContentSignerBuilder("SHA256withRSA").build(keyPair.private)
|
||||
|
||||
val name = X500Name("CN=$commonName")
|
||||
val certificateHolder = X509v3CertificateBuilder(
|
||||
name,
|
||||
BigInteger.valueOf(SecureRandom().nextLong()),
|
||||
Date(System.currentTimeMillis()),
|
||||
validUntil,
|
||||
Locale.ENGLISH,
|
||||
name,
|
||||
SubjectPublicKeyInfo.getInstance(keyPair.public.encoded),
|
||||
).build(contentSigner)
|
||||
val certificate = JcaX509CertificateConverter().getCertificate(certificateHolder)
|
||||
|
||||
return PrivateKeyCertificatePair(keyPair.private, certificate)
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a [PrivateKeyCertificatePair] from a keystore entry.
|
||||
*
|
||||
* @param keyStore The keystore to read the entry from.
|
||||
* @param keyStoreEntryAlias The alias of the key store entry to read.
|
||||
* @param keyStoreEntryPassword The password for recovering the signing key.
|
||||
*
|
||||
* @return The read [PrivateKeyCertificatePair].
|
||||
*
|
||||
* @throws IllegalArgumentException If the keystore does not contain the given alias or the password is invalid.
|
||||
*
|
||||
* @see PrivateKeyCertificatePair
|
||||
* @see KeyStore
|
||||
*/
|
||||
fun readPrivateKeyCertificatePair(
|
||||
keyStore: KeyStore,
|
||||
keyStoreEntryAlias: String,
|
||||
keyStoreEntryPassword: String,
|
||||
): PrivateKeyCertificatePair {
|
||||
logger.fine("Reading key and certificate pair from keystore entry $keyStoreEntryAlias")
|
||||
|
||||
if (!keyStore.containsAlias(keyStoreEntryAlias)) {
|
||||
throw IllegalArgumentException("Keystore does not contain entry with alias $keyStoreEntryAlias")
|
||||
}
|
||||
|
||||
// Read the private key and certificate from the keystore.
|
||||
|
||||
val privateKey =
|
||||
try {
|
||||
keyStore.getKey(keyStoreEntryAlias, keyStoreEntryPassword.toCharArray()) as PrivateKey
|
||||
} catch (exception: UnrecoverableKeyException) {
|
||||
throw IllegalArgumentException("Invalid password for keystore entry $keyStoreEntryAlias")
|
||||
}
|
||||
|
||||
val certificate = keyStore.getCertificate(keyStoreEntryAlias) as X509Certificate
|
||||
|
||||
return PrivateKeyCertificatePair(privateKey, certificate)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new [Signer].
|
||||
*
|
||||
* @param signer The name of the signer.
|
||||
* @param privateKeyCertificatePair The private key and certificate pair to use for signing.
|
||||
*
|
||||
* @return The new [Signer].
|
||||
*
|
||||
* @see PrivateKeyCertificatePair
|
||||
* @see Signer
|
||||
*/
|
||||
fun newApkSigner(
|
||||
signer: String,
|
||||
privateKeyCertificatePair: PrivateKeyCertificatePair,
|
||||
) = Signer(
|
||||
com.android.apksig.ApkSigner.Builder(
|
||||
listOf(
|
||||
SignerConfig.Builder(
|
||||
signer,
|
||||
privateKeyCertificatePair.privateKey,
|
||||
listOf(privateKeyCertificatePair.certificate),
|
||||
).build(),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
/**
|
||||
* Read a [PrivateKeyCertificatePair] from a keystore entry.
|
||||
*
|
||||
* @param keyStore The keystore to read the entry from.
|
||||
* @param keyStoreEntryAlias The alias of the key store entry to read.
|
||||
* @param keyStoreEntryPassword The password for recovering the signing key.
|
||||
*
|
||||
* @return The read [PrivateKeyCertificatePair].
|
||||
*
|
||||
* @throws IllegalArgumentException If the keystore does not contain the given alias or the password is invalid.
|
||||
*/
|
||||
@Deprecated("This method will be removed in the future.")
|
||||
fun readKeyCertificatePair(
|
||||
keyStore: KeyStore,
|
||||
keyStoreEntryAlias: String,
|
||||
keyStoreEntryPassword: String,
|
||||
) = readPrivateKeyCertificatePair(keyStore, keyStoreEntryAlias, keyStoreEntryPassword)
|
||||
|
||||
/**
|
||||
* Create a new keystore with a new keypair and saves it to the given [keyStoreOutputStream].
|
||||
*
|
||||
* @param keyStoreOutputStream The stream to write the keystore to.
|
||||
* @param keyStorePassword The password for the keystore.
|
||||
* @param entries The entries to add to the keystore.
|
||||
*/
|
||||
@Deprecated("This method will be removed in the future.")
|
||||
fun newKeyStore(
|
||||
keyStoreOutputStream: OutputStream,
|
||||
keyStorePassword: String?,
|
||||
entries: Set<KeyStoreEntry>,
|
||||
) = newKeyStore(entries).store(
|
||||
keyStoreOutputStream,
|
||||
keyStorePassword?.toCharArray(),
|
||||
)
|
||||
|
||||
/**
|
||||
* Create a new [Signer].
|
||||
*
|
||||
* @param privateKeyCertificatePair The private key and certificate pair to use for signing.
|
||||
*
|
||||
* @return The new [Signer].
|
||||
*
|
||||
* @see PrivateKeyCertificatePair
|
||||
* @see Signer
|
||||
*/
|
||||
@Deprecated("This method will be removed in the future.")
|
||||
fun newApkSigner(privateKeyCertificatePair: PrivateKeyCertificatePair) =
|
||||
Signer(
|
||||
SigningExtension(
|
||||
SigningOptions.builder()
|
||||
.setMinSdkVersion(21) // TODO: Extracting from the target APK would be ideal.
|
||||
.setV1SigningEnabled(true)
|
||||
.setV2SigningEnabled(true)
|
||||
.setCertificates(privateKeyCertificatePair.certificate)
|
||||
.setKey(privateKeyCertificatePair.privateKey)
|
||||
.build(),
|
||||
),
|
||||
)
|
||||
|
||||
/**
|
||||
* Create a new [Signer].
|
||||
*
|
||||
* @param signer The name of the signer.
|
||||
* @param keyStore The keystore to use for signing.
|
||||
* @param keyStoreEntryAlias The alias of the key store entry to use for signing.
|
||||
* @param keyStoreEntryPassword The password for recovering the signing key.
|
||||
*
|
||||
* @return The new [Signer].
|
||||
*
|
||||
* @see KeyStore
|
||||
* @see Signer
|
||||
*/
|
||||
@Deprecated("This method will be removed in the future.")
|
||||
fun newApkSigner(
|
||||
signer: String,
|
||||
keyStore: KeyStore,
|
||||
keyStoreEntryAlias: String,
|
||||
keyStoreEntryPassword: String,
|
||||
) = newApkSigner(signer, readKeyCertificatePair(keyStore, keyStoreEntryAlias, keyStoreEntryPassword))
|
||||
|
||||
/**
|
||||
* Create a new [Signer].
|
||||
*
|
||||
* @param keyStore The keystore to use for signing.
|
||||
* @param keyStoreEntryAlias The alias of the key store entry to use for signing.
|
||||
* @param keyStoreEntryPassword The password for recovering the signing key.
|
||||
*
|
||||
* @return The new [Signer].
|
||||
*
|
||||
* @see KeyStore
|
||||
* @see Signer
|
||||
*/
|
||||
@Deprecated("This method will be removed in the future.")
|
||||
fun newApkSigner(
|
||||
keyStore: KeyStore,
|
||||
keyStoreEntryAlias: String,
|
||||
keyStoreEntryPassword: String,
|
||||
) = newApkSigner("ReVanced", readKeyCertificatePair(keyStore, keyStoreEntryAlias, keyStoreEntryPassword))
|
||||
|
||||
/**
|
||||
* An entry in a keystore.
|
||||
*
|
||||
* @param alias The alias of the entry.
|
||||
* @param password The password for recovering the signing key.
|
||||
* @param privateKeyCertificatePair The private key and certificate pair.
|
||||
*
|
||||
* @see PrivateKeyCertificatePair
|
||||
*/
|
||||
class KeyStoreEntry(
|
||||
val alias: String,
|
||||
val password: String,
|
||||
val privateKeyCertificatePair: PrivateKeyCertificatePair,
|
||||
)
|
||||
|
||||
/**
|
||||
* A private key and certificate pair.
|
||||
*
|
||||
* @param privateKey The private key.
|
||||
* @param certificate The certificate.
|
||||
*/
|
||||
class PrivateKeyCertificatePair(
|
||||
val privateKey: PrivateKey,
|
||||
val certificate: X509Certificate,
|
||||
)
|
||||
|
||||
class Signer {
|
||||
private val signerBuilder: com.android.apksig.ApkSigner.Builder?
|
||||
private val signingExtension: SigningExtension?
|
||||
|
||||
internal constructor(signerBuilder: com.android.apksig.ApkSigner.Builder) {
|
||||
this.signerBuilder = signerBuilder
|
||||
signingExtension = null
|
||||
}
|
||||
|
||||
fun signApk(inputApkFile: File, outputApkFile: File) {
|
||||
logger.info("Signing APK")
|
||||
|
||||
signerBuilder?.setInputApk(inputApkFile)?.setOutputApk(outputApkFile)?.build()?.sign()
|
||||
}
|
||||
|
||||
@Deprecated("This constructor will be removed in the future.")
|
||||
internal constructor(signingExtension: SigningExtension) {
|
||||
signerBuilder = null
|
||||
this.signingExtension = signingExtension
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign an APK file.
|
||||
*
|
||||
* @param apkFile The APK file to sign.
|
||||
*/
|
||||
@Deprecated("This method will be removed in the future.")
|
||||
fun signApk(apkFile: File) = ZFile.openReadWrite(apkFile).use {
|
||||
@Suppress("DEPRECATION")
|
||||
signApk(it)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign an APK file.
|
||||
*
|
||||
* @param apkZFile The APK [ZFile] to sign.
|
||||
*/
|
||||
@Deprecated("This method will be removed in the future.")
|
||||
fun signApk(apkZFile: ZFile) {
|
||||
logger.info("Signing ${apkZFile.file.name}")
|
||||
|
||||
signingExtension?.register(apkZFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
281
src/commonMain/kotlin/app/revanced/library/ApkUtils.kt
Normal file
281
src/commonMain/kotlin/app/revanced/library/ApkUtils.kt
Normal file
@@ -0,0 +1,281 @@
|
||||
package app.revanced.library
|
||||
|
||||
import app.revanced.library.ApkSigner.newPrivateKeyCertificatePair
|
||||
import app.revanced.patcher.PatcherResult
|
||||
import com.android.tools.build.apkzlib.zip.AlignmentRules
|
||||
import com.android.tools.build.apkzlib.zip.StoredEntry
|
||||
import com.android.tools.build.apkzlib.zip.ZFile
|
||||
import com.android.tools.build.apkzlib.zip.ZFileOptions
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
import java.util.logging.Logger
|
||||
import kotlin.time.Duration.Companion.days
|
||||
|
||||
/**
|
||||
* Utility functions to work with APK files.
|
||||
*/
|
||||
@Suppress("MemberVisibilityCanBePrivate", "unused")
|
||||
object ApkUtils {
|
||||
private val logger = Logger.getLogger(ApkUtils::class.java.name)
|
||||
|
||||
private const val LIBRARY_EXTENSION = ".so"
|
||||
|
||||
// Alignment for native libraries.
|
||||
private const val LIBRARY_ALIGNMENT = 1024 * 4
|
||||
|
||||
// Alignment for all other files.
|
||||
private const val DEFAULT_ALIGNMENT = 4
|
||||
|
||||
// Prefix for resources.
|
||||
private const val RES_PREFIX = "res/"
|
||||
|
||||
private val zFileOptions =
|
||||
ZFileOptions().setAlignmentRule(
|
||||
AlignmentRules.compose(
|
||||
AlignmentRules.constantForSuffix(LIBRARY_EXTENSION, LIBRARY_ALIGNMENT),
|
||||
AlignmentRules.constant(DEFAULT_ALIGNMENT),
|
||||
),
|
||||
)
|
||||
|
||||
/**
|
||||
* Applies the [PatcherResult] to the given [apkFile].
|
||||
*
|
||||
* The order of operation is as follows:
|
||||
* 1. Write patched dex files.
|
||||
* 2. Delete all resources in the target APK
|
||||
* 3. Merge resources.apk compiled by AAPT.
|
||||
* 4. Write raw resources.
|
||||
* 5. Delete resources staged for deletion.
|
||||
* 6. Realign the APK.
|
||||
*
|
||||
* @param apkFile The file to apply the patched files to.
|
||||
*/
|
||||
fun PatcherResult.applyTo(apkFile: File) {
|
||||
ZFile.openReadWrite(apkFile, zFileOptions).use { targetApkZFile ->
|
||||
dexFiles.forEach { dexFile ->
|
||||
targetApkZFile.add(dexFile.name, dexFile.stream)
|
||||
}
|
||||
|
||||
resources?.let { resources ->
|
||||
// Add resources compiled by AAPT.
|
||||
resources.resourcesApk?.let { resourcesApk ->
|
||||
ZFile.openReadOnly(resourcesApk).use { resourcesApkZFile ->
|
||||
// Delete all resources in the target APK before merging the new ones.
|
||||
// This is necessary because the resources.apk renames resources.
|
||||
// So unless, the old resources are deleted, there will be orphaned resources in the APK.
|
||||
// It is not necessary, but for the sake of cleanliness, it is done.
|
||||
targetApkZFile.entries().filter { entry ->
|
||||
entry.centralDirectoryHeader.name.startsWith(RES_PREFIX)
|
||||
}.forEach(StoredEntry::delete)
|
||||
|
||||
targetApkZFile.mergeFrom(resourcesApkZFile) { false }
|
||||
}
|
||||
}
|
||||
|
||||
// Add resources not compiled by AAPT.
|
||||
resources.otherResources?.let { otherResources ->
|
||||
targetApkZFile.addAllRecursively(otherResources) { file ->
|
||||
file.relativeTo(otherResources).invariantSeparatorsPath !in resources.doNotCompress
|
||||
}
|
||||
}
|
||||
|
||||
// Delete resources that were staged for deletion.
|
||||
if (resources.deleteResources.isNotEmpty()) {
|
||||
targetApkZFile.entries().filter { entry ->
|
||||
resources.deleteResources.any { shouldDelete -> shouldDelete(entry.centralDirectoryHeader.name) }
|
||||
}.forEach(StoredEntry::delete)
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("Aligning APK")
|
||||
|
||||
targetApkZFile.realign()
|
||||
|
||||
logger.fine("Writing changes")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new private key and certificate pair and saves it to the keystore in [keyStoreDetails].
|
||||
*
|
||||
* @param privateKeyCertificatePairDetails The details for the private key and certificate pair.
|
||||
* @param keyStoreDetails The details for the keystore.
|
||||
*
|
||||
* @return The newly created private key and certificate pair.
|
||||
*/
|
||||
@Deprecated("This method will be removed in the future.")
|
||||
fun newPrivateKeyCertificatePair(
|
||||
privateKeyCertificatePairDetails: PrivateKeyCertificatePairDetails,
|
||||
keyStoreDetails: KeyStoreDetails,
|
||||
) = newPrivateKeyCertificatePair(
|
||||
privateKeyCertificatePairDetails.commonName,
|
||||
privateKeyCertificatePairDetails.validUntil,
|
||||
).also { privateKeyCertificatePair ->
|
||||
ApkSigner.newKeyStore(
|
||||
setOf(
|
||||
ApkSigner.KeyStoreEntry(
|
||||
keyStoreDetails.alias,
|
||||
keyStoreDetails.password,
|
||||
privateKeyCertificatePair,
|
||||
),
|
||||
),
|
||||
).store(
|
||||
keyStoreDetails.keyStore.outputStream(),
|
||||
keyStoreDetails.keyStorePassword?.toCharArray(),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the private key and certificate pair from an existing keystore.
|
||||
*
|
||||
* @param keyStoreDetails The details for the keystore.
|
||||
*
|
||||
* @return The private key and certificate pair.
|
||||
*/
|
||||
@Deprecated("This method will be removed in the future.")
|
||||
fun readPrivateKeyCertificatePairFromKeyStore(
|
||||
keyStoreDetails: KeyStoreDetails,
|
||||
) = ApkSigner.readPrivateKeyCertificatePair(
|
||||
ApkSigner.readKeyStore(
|
||||
keyStoreDetails.keyStore.inputStream(),
|
||||
keyStoreDetails.keyStorePassword,
|
||||
),
|
||||
keyStoreDetails.alias,
|
||||
keyStoreDetails.password,
|
||||
)
|
||||
|
||||
/**
|
||||
* Signs [inputApkFile] with the given options and saves the signed apk to [outputApkFile].
|
||||
* If [KeyStoreDetails.keyStore] does not exist,
|
||||
* a new private key and certificate pair will be created and saved to the keystore.
|
||||
*
|
||||
* @param inputApkFile The apk file to sign.
|
||||
* @param outputApkFile The file to save the signed apk to.
|
||||
* @param signer The name of the signer.
|
||||
* @param keyStoreDetails The details for the keystore.
|
||||
*/
|
||||
fun signApk(
|
||||
inputApkFile: File,
|
||||
outputApkFile: File,
|
||||
signer: String,
|
||||
keyStoreDetails: KeyStoreDetails,
|
||||
) = ApkSigner.newApkSigner(
|
||||
signer,
|
||||
if (keyStoreDetails.keyStore.exists()) {
|
||||
readPrivateKeyCertificatePairFromKeyStore(keyStoreDetails)
|
||||
} else {
|
||||
newPrivateKeyCertificatePair(PrivateKeyCertificatePairDetails(), keyStoreDetails)
|
||||
},
|
||||
).signApk(inputApkFile, outputApkFile)
|
||||
|
||||
@Deprecated("This method will be removed in the future.")
|
||||
private fun readOrNewPrivateKeyCertificatePair(
|
||||
signingOptions: SigningOptions,
|
||||
): ApkSigner.PrivateKeyCertificatePair {
|
||||
val privateKeyCertificatePairDetails = PrivateKeyCertificatePairDetails(
|
||||
signingOptions.alias,
|
||||
PrivateKeyCertificatePairDetails().validUntil,
|
||||
)
|
||||
val keyStoreDetails = KeyStoreDetails(
|
||||
signingOptions.keyStore,
|
||||
signingOptions.keyStorePassword,
|
||||
signingOptions.alias,
|
||||
signingOptions.password,
|
||||
)
|
||||
|
||||
return if (keyStoreDetails.keyStore.exists()) {
|
||||
readPrivateKeyCertificatePairFromKeyStore(keyStoreDetails)
|
||||
} else {
|
||||
newPrivateKeyCertificatePair(privateKeyCertificatePairDetails, keyStoreDetails)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Signs [inputApkFile] with the given options and saves the signed apk to [outputApkFile].
|
||||
*
|
||||
* @param inputApkFile The apk file to sign.
|
||||
* @param outputApkFile The file to save the signed apk to.
|
||||
* @param signer The name of the signer.
|
||||
* @param privateKeyCertificatePair The private key and certificate pair to use for signing.
|
||||
*/
|
||||
@Deprecated("This method will be removed in the future.")
|
||||
fun sign(
|
||||
inputApkFile: File,
|
||||
outputApkFile: File,
|
||||
signer: String,
|
||||
privateKeyCertificatePair: ApkSigner.PrivateKeyCertificatePair,
|
||||
) = ApkSigner.newApkSigner(
|
||||
signer,
|
||||
privateKeyCertificatePair,
|
||||
).signApk(inputApkFile, outputApkFile)
|
||||
|
||||
/**
|
||||
* Signs the apk file with the given options.
|
||||
*
|
||||
* @param signingOptions The options to use for signing.
|
||||
*/
|
||||
@Deprecated("This method will be removed in the future.")
|
||||
fun File.sign(signingOptions: SigningOptions) = ApkSigner.newApkSigner(
|
||||
signingOptions.signer,
|
||||
readOrNewPrivateKeyCertificatePair(signingOptions),
|
||||
).signApk(this)
|
||||
|
||||
/**
|
||||
* Signs [inputApkFile] with the given options and saves the signed apk to [outputApkFile].
|
||||
*
|
||||
* @param inputApkFile The apk file to sign.
|
||||
* @param outputApkFile The file to save the signed apk to.
|
||||
* @param signingOptions The options to use for signing.
|
||||
*/
|
||||
@Deprecated("This method will be removed in the future.")
|
||||
fun sign(inputApkFile: File, outputApkFile: File, signingOptions: SigningOptions) = sign(
|
||||
inputApkFile,
|
||||
outputApkFile,
|
||||
signingOptions.signer,
|
||||
readOrNewPrivateKeyCertificatePair(signingOptions),
|
||||
)
|
||||
|
||||
/**
|
||||
* Options for signing an apk.
|
||||
*
|
||||
* @param keyStore The keystore to use for signing.
|
||||
* @param keyStorePassword The password for the keystore.
|
||||
* @param alias The alias of the key store entry to use for signing.
|
||||
* @param password The password for recovering the signing key.
|
||||
* @param signer The name of the signer.
|
||||
*/
|
||||
@Deprecated("This class will be removed in the future.")
|
||||
class SigningOptions(
|
||||
val keyStore: File,
|
||||
val keyStorePassword: String?,
|
||||
val alias: String = "ReVanced Key",
|
||||
val password: String = "",
|
||||
val signer: String = "ReVanced",
|
||||
)
|
||||
|
||||
/**
|
||||
* Details for a keystore.
|
||||
*
|
||||
* @param keyStore The file to save the keystore to.
|
||||
* @param keyStorePassword The password for the keystore.
|
||||
* @param alias The alias of the key store entry to use for signing.
|
||||
* @param password The password for recovering the signing key.
|
||||
*/
|
||||
class KeyStoreDetails(
|
||||
val keyStore: File,
|
||||
val keyStorePassword: String? = null,
|
||||
val alias: String = "ReVanced Key",
|
||||
val password: String = "",
|
||||
)
|
||||
|
||||
/**
|
||||
* Details for a private key and certificate pair.
|
||||
*
|
||||
* @param commonName The common name for the certificate saved in the keystore.
|
||||
* @param validUntil The date until which the certificate is valid.
|
||||
*/
|
||||
class PrivateKeyCertificatePairDetails(
|
||||
val commonName: String = "ReVanced",
|
||||
val validUntil: Date = Date(System.currentTimeMillis() + (365.days * 8).inWholeMilliseconds * 24),
|
||||
)
|
||||
}
|
||||
43
src/commonMain/kotlin/app/revanced/library/Commands.kt
Normal file
43
src/commonMain/kotlin/app/revanced/library/Commands.kt
Normal file
@@ -0,0 +1,43 @@
|
||||
@file:Suppress("DeprecatedCallableAddReplaceWith")
|
||||
|
||||
package app.revanced.library
|
||||
|
||||
import app.revanced.library.installation.command.AdbShellCommandRunner
|
||||
import se.vidstige.jadb.JadbDevice
|
||||
import se.vidstige.jadb.ShellProcessBuilder
|
||||
import java.io.File
|
||||
|
||||
@Deprecated("Do not use this anymore. Instead use AdbCommandRunner.")
|
||||
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())
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
@Deprecated("Use AdbShellCommandRunner instead.")
|
||||
internal fun JadbDevice.run(
|
||||
command: String,
|
||||
su: Boolean = true,
|
||||
) = buildCommand(command, su).start()
|
||||
|
||||
@Deprecated("Use AdbShellCommandRunner instead.")
|
||||
internal fun JadbDevice.hasSu() = AdbShellCommandRunner(this).hasRootPermission()
|
||||
|
||||
@Deprecated("Use AdbShellCommandRunner instead.")
|
||||
internal fun JadbDevice.push(
|
||||
file: File,
|
||||
targetFilePath: String,
|
||||
) = AdbShellCommandRunner(this).move(file, targetFilePath)
|
||||
|
||||
@Deprecated("Use AdbShellCommandRunner instead.")
|
||||
internal fun JadbDevice.createFile(
|
||||
targetFile: String,
|
||||
content: String,
|
||||
) = AdbShellCommandRunner(this).write(content.byteInputStream(), targetFile)
|
||||
120
src/commonMain/kotlin/app/revanced/library/Options.kt
Normal file
120
src/commonMain/kotlin/app/revanced/library/Options.kt
Normal file
@@ -0,0 +1,120 @@
|
||||
@file:Suppress("MemberVisibilityCanBePrivate")
|
||||
|
||||
package app.revanced.library
|
||||
|
||||
import app.revanced.library.Options.Patch.Option
|
||||
import app.revanced.patcher.PatchSet
|
||||
import app.revanced.patcher.patch.options.PatchOptionException
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import java.io.File
|
||||
import java.util.logging.Logger
|
||||
|
||||
@Suppress("unused")
|
||||
object Options {
|
||||
private val logger = Logger.getLogger(Options::class.java.name)
|
||||
|
||||
private val mapper = jacksonObjectMapper()
|
||||
|
||||
/**
|
||||
* Serializes the options for a set of patches.
|
||||
*
|
||||
* @param patches The set of patches to serialize.
|
||||
* @param prettyPrint Whether to pretty print the JSON.
|
||||
* @return The JSON string containing the options.
|
||||
*/
|
||||
fun serialize(
|
||||
patches: PatchSet,
|
||||
prettyPrint: Boolean = false,
|
||||
): String =
|
||||
patches
|
||||
.filter { it.options.any() }
|
||||
.map { patch ->
|
||||
Patch(
|
||||
patch.name!!,
|
||||
patch.options.values.map { option ->
|
||||
val optionValue =
|
||||
try {
|
||||
option.value
|
||||
} catch (e: PatchOptionException) {
|
||||
logger.warning("Using default option value for the ${patch.name} patch: ${e.message}")
|
||||
option.default
|
||||
}
|
||||
|
||||
Option(option.key, optionValue)
|
||||
},
|
||||
)
|
||||
}
|
||||
// See https://github.com/revanced/revanced-patches/pull/2434/commits/60e550550b7641705e81aa72acfc4faaebb225e7.
|
||||
.distinctBy { it.patchName }
|
||||
.let {
|
||||
if (prettyPrint) {
|
||||
mapper.writerWithDefaultPrettyPrinter().writeValueAsString(it)
|
||||
} else {
|
||||
mapper.writeValueAsString(it)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserializes the options to a set of patches.
|
||||
*
|
||||
* @param json The JSON string containing the options.
|
||||
* @return A set of [Patch]s.
|
||||
* @see Patch
|
||||
*/
|
||||
fun deserialize(json: String): Array<Patch> = mapper.readValue(json, Array<Patch>::class.java)
|
||||
|
||||
/**
|
||||
* Sets the options for a set of patches.
|
||||
*
|
||||
* @param json The JSON string containing the options.
|
||||
*/
|
||||
fun PatchSet.setOptions(json: String) {
|
||||
filter { it.options.any() }.let { patches ->
|
||||
if (patches.isEmpty()) return
|
||||
|
||||
val jsonPatches =
|
||||
deserialize(json).associate {
|
||||
it.patchName to it.options.associate { option -> option.key to option.value }
|
||||
}
|
||||
|
||||
patches.forEach { patch ->
|
||||
jsonPatches[patch.name]?.let { jsonPatchOptions ->
|
||||
jsonPatchOptions.forEach { (option, value) ->
|
||||
try {
|
||||
patch.options[option] = value
|
||||
} catch (e: PatchOptionException) {
|
||||
logger.warning("Could not set option value for the ${patch.name} patch: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the options for a set of patches.
|
||||
*
|
||||
* @param file The file containing the JSON string containing the options.
|
||||
* @see setOptions
|
||||
*/
|
||||
fun PatchSet.setOptions(file: File) = setOptions(file.readText())
|
||||
|
||||
/**
|
||||
* Data class for a patch and its [Option]s.
|
||||
*
|
||||
* @property patchName The name of the patch.
|
||||
* @property options The [Option]s for the patch.
|
||||
*/
|
||||
class Patch internal constructor(
|
||||
val patchName: String,
|
||||
val options: List<Option>,
|
||||
) {
|
||||
/**
|
||||
* Data class for patch option.
|
||||
*
|
||||
* @property key The name of the option.
|
||||
* @property value The value of the option.
|
||||
*/
|
||||
class Option internal constructor(val key: String, val value: Any?)
|
||||
}
|
||||
}
|
||||
169
src/commonMain/kotlin/app/revanced/library/PatchUtils.kt
Normal file
169
src/commonMain/kotlin/app/revanced/library/PatchUtils.kt
Normal file
@@ -0,0 +1,169 @@
|
||||
package app.revanced.library
|
||||
|
||||
import app.revanced.patcher.PatchSet
|
||||
import app.revanced.patcher.patch.Patch
|
||||
import app.revanced.patcher.patch.options.PatchOption
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import kotlin.reflect.jvm.jvmName
|
||||
|
||||
typealias PackageName = String
|
||||
typealias Version = String
|
||||
typealias Count = Int
|
||||
|
||||
typealias VersionMap = LinkedHashMap<Version, Count>
|
||||
typealias PackageNameMap = Map<PackageName, VersionMap>
|
||||
|
||||
/**
|
||||
* Utility functions for working with patches.
|
||||
*/
|
||||
@Suppress("MemberVisibilityCanBePrivate", "unused")
|
||||
object PatchUtils {
|
||||
/**
|
||||
* Get the count of versions for each compatible package from a supplied set of [patches] ordered by the most common version.
|
||||
*
|
||||
* @param patches The set of patches to check.
|
||||
* @param packageNames The names of the compatible packages to include. If null, all packages will be included.
|
||||
* @param countUnusedPatches Whether to count patches that are not used.
|
||||
* @return A map of package names to a map of versions to their count.
|
||||
*/
|
||||
fun getMostCommonCompatibleVersions(
|
||||
patches: PatchSet,
|
||||
packageNames: Set<String>? = null,
|
||||
countUnusedPatches: Boolean = false,
|
||||
): PackageNameMap =
|
||||
buildMap {
|
||||
fun filterWantedPackages(compatiblePackages: Iterable<Patch.CompatiblePackage>): Iterable<Patch.CompatiblePackage> {
|
||||
val wantedPackages = packageNames?.toHashSet() ?: return compatiblePackages
|
||||
return compatiblePackages.filter { it.name in wantedPackages }
|
||||
}
|
||||
|
||||
patches
|
||||
.filter { it.use || countUnusedPatches }
|
||||
.flatMap { it.compatiblePackages ?: emptyList() }
|
||||
.let(::filterWantedPackages)
|
||||
.forEach { compatiblePackage ->
|
||||
if (compatiblePackage.versions?.isEmpty() == true) {
|
||||
return@forEach
|
||||
}
|
||||
|
||||
val versionMap = getOrPut(compatiblePackage.name) { linkedMapOf() }
|
||||
|
||||
compatiblePackage.versions?.let { versions ->
|
||||
versions.forEach { version ->
|
||||
versionMap[version] = versionMap.getOrDefault(version, 0) + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort the version maps by the most common version.
|
||||
forEach { (packageName, versionMap) ->
|
||||
this[packageName] =
|
||||
versionMap
|
||||
.asIterable()
|
||||
.sortedWith(compareByDescending { it.value })
|
||||
.associate { it.key to it.value } as VersionMap
|
||||
}
|
||||
}
|
||||
|
||||
object Json {
|
||||
private val mapper = jacksonObjectMapper()
|
||||
|
||||
/**
|
||||
* Serializes a set of [Patch]es to a JSON string and writes it to an output stream.
|
||||
*
|
||||
* @param patches The set of [Patch]es to serialize.
|
||||
* @param transform A function to transform the [Patch]es to [JsonPatch]es.
|
||||
* @param prettyPrint Whether to pretty print the JSON.
|
||||
* @param outputStream The output stream to write the JSON to.
|
||||
*/
|
||||
fun serialize(
|
||||
patches: PatchSet,
|
||||
transform: (Patch<*>) -> JsonPatch = { patch -> FullJsonPatch.fromPatch(patch) },
|
||||
prettyPrint: Boolean = false,
|
||||
outputStream: OutputStream,
|
||||
) {
|
||||
patches.map(transform).let { transformed ->
|
||||
if (prettyPrint) {
|
||||
mapper.writerWithDefaultPrettyPrinter().writeValue(outputStream, transformed)
|
||||
} else {
|
||||
mapper.writeValue(outputStream, transformed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserializes a JSON string to a set of [FullJsonPatch]es from an input stream.
|
||||
*
|
||||
* @param inputStream The input stream to read the JSON from.
|
||||
* @param jsonPatchElementClass The class of the [JsonPatch]es to deserialize.
|
||||
* @return A set of [JsonPatch]es.
|
||||
* @see FullJsonPatch
|
||||
*/
|
||||
fun <T : JsonPatch> deserialize(
|
||||
inputStream: InputStream,
|
||||
jsonPatchElementClass: Class<T>,
|
||||
): Set<T> =
|
||||
mapper.readValue(
|
||||
inputStream,
|
||||
mapper.typeFactory.constructCollectionType(Set::class.java, jsonPatchElementClass),
|
||||
)
|
||||
|
||||
interface JsonPatch
|
||||
|
||||
/**
|
||||
* A JSON representation of a [Patch].
|
||||
* @see Patch
|
||||
*/
|
||||
class FullJsonPatch internal constructor(
|
||||
val name: String?,
|
||||
val description: String?,
|
||||
val compatiblePackages: Set<Patch.CompatiblePackage>?,
|
||||
val dependencies: Set<String>?,
|
||||
val use: Boolean,
|
||||
var requiresIntegrations: Boolean,
|
||||
val options: Map<String, FullJsonPatchOption<*>>,
|
||||
) : JsonPatch {
|
||||
companion object {
|
||||
fun fromPatch(patch: Patch<*>) =
|
||||
FullJsonPatch(
|
||||
patch.name,
|
||||
patch.description,
|
||||
patch.compatiblePackages,
|
||||
buildSet { patch.dependencies?.forEach { add(it.jvmName) } },
|
||||
patch.use,
|
||||
patch.requiresIntegrations,
|
||||
patch.options.mapValues { FullJsonPatchOption.fromPatchOption(it.value) },
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A JSON representation of a [PatchOption].
|
||||
* @see PatchOption
|
||||
*/
|
||||
class FullJsonPatchOption<T> internal constructor(
|
||||
val key: String,
|
||||
val default: T?,
|
||||
val values: Map<String, T?>?,
|
||||
val title: String?,
|
||||
val description: String?,
|
||||
val required: Boolean,
|
||||
val valueType: String,
|
||||
) {
|
||||
companion object {
|
||||
fun fromPatchOption(option: PatchOption<*>) =
|
||||
FullJsonPatchOption(
|
||||
option.key,
|
||||
option.default,
|
||||
option.values,
|
||||
option.title,
|
||||
option.description,
|
||||
option.required,
|
||||
option.valueType,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
18
src/commonMain/kotlin/app/revanced/library/Utils.kt
Normal file
18
src/commonMain/kotlin/app/revanced/library/Utils.kt
Normal file
@@ -0,0 +1,18 @@
|
||||
package app.revanced.library
|
||||
|
||||
/**
|
||||
* Utils for the library.
|
||||
*/
|
||||
@Suppress("unused")
|
||||
object Utils {
|
||||
/**
|
||||
* True if the environment is Android.
|
||||
*/
|
||||
val isAndroidEnvironment =
|
||||
try {
|
||||
Class.forName("android.app.Application")
|
||||
true
|
||||
} catch (e: ClassNotFoundException) {
|
||||
false
|
||||
}
|
||||
}
|
||||
144
src/commonMain/kotlin/app/revanced/library/adb/AdbManager.kt
Normal file
144
src/commonMain/kotlin/app/revanced/library/adb/AdbManager.kt
Normal file
@@ -0,0 +1,144 @@
|
||||
@file:Suppress("DEPRECATION")
|
||||
|
||||
package app.revanced.library.adb
|
||||
|
||||
import app.revanced.library.adb.AdbManager.Apk
|
||||
import app.revanced.library.installation.installer.AdbInstaller
|
||||
import app.revanced.library.installation.installer.AdbRootInstaller
|
||||
import app.revanced.library.installation.installer.Constants.PLACEHOLDER
|
||||
import app.revanced.library.installation.installer.Installer
|
||||
import app.revanced.library.run
|
||||
import se.vidstige.jadb.JadbDevice
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* [AdbManager] to install and uninstall [Apk] files.
|
||||
*
|
||||
* @param deviceSerial The serial of the device. If null, the first connected device will be used.
|
||||
*/
|
||||
@Deprecated("Use an implementation of Installer instead.")
|
||||
@Suppress("unused")
|
||||
sealed class AdbManager private constructor(
|
||||
@Suppress("UNUSED_PARAMETER") deviceSerial: String?,
|
||||
) {
|
||||
protected abstract val installer: Installer<*, *>
|
||||
|
||||
/**
|
||||
* Installs the [Apk] file.
|
||||
*
|
||||
* @param apk The [Apk] file.
|
||||
*/
|
||||
@Suppress("DeprecatedCallableAddReplaceWith")
|
||||
@Deprecated("Use Installer.install instead.")
|
||||
open fun install(apk: Apk) = suspend {
|
||||
installer.install(Installer.Apk(apk.file, apk.packageName))
|
||||
}
|
||||
|
||||
/**
|
||||
* Uninstalls the package.
|
||||
*
|
||||
* @param packageName The package name.
|
||||
*/
|
||||
@Suppress("DeprecatedCallableAddReplaceWith")
|
||||
@Deprecated("Use Installer.uninstall instead.")
|
||||
open fun uninstall(packageName: String) = suspend {
|
||||
installer.uninstall(packageName)
|
||||
}
|
||||
|
||||
@Deprecated("Use Installer instead.")
|
||||
companion object {
|
||||
/**
|
||||
* Gets an [AdbManager] for the supplied device serial.
|
||||
*
|
||||
* @param deviceSerial The device serial. If null, the first connected device will be used.
|
||||
* @param root Whether to use root or not.
|
||||
* @return The [AdbManager].
|
||||
* @throws DeviceNotFoundException If the device can not be found.
|
||||
*/
|
||||
@Suppress("DeprecatedCallableAddReplaceWith")
|
||||
@Deprecated("This is deprecated.")
|
||||
fun getAdbManager(
|
||||
deviceSerial: String? = null,
|
||||
root: Boolean = false,
|
||||
): AdbManager = if (root) RootAdbManager(deviceSerial) else UserAdbManager(deviceSerial)
|
||||
}
|
||||
|
||||
/**
|
||||
* Adb manager for rooted devices.
|
||||
*
|
||||
* @param deviceSerial The device serial. If null, the first connected device will be used.
|
||||
*/
|
||||
@Deprecated("Use AdbRootInstaller instead.", ReplaceWith("AdbRootInstaller(deviceSerial)"))
|
||||
class RootAdbManager internal constructor(deviceSerial: String?) : AdbManager(deviceSerial) {
|
||||
override val installer = AdbRootInstaller(deviceSerial)
|
||||
|
||||
@Suppress("DeprecatedCallableAddReplaceWith")
|
||||
@Deprecated("Use AdbRootInstaller.install instead.")
|
||||
override fun install(apk: Apk) = suspend {
|
||||
installer.install(Installer.Apk(apk.file, apk.packageName))
|
||||
}
|
||||
|
||||
@Suppress("DeprecatedCallableAddReplaceWith")
|
||||
@Deprecated("Use AdbRootInstaller.uninstall instead.")
|
||||
override fun uninstall(packageName: String) = suspend {
|
||||
installer.uninstall(packageName)
|
||||
}
|
||||
|
||||
@Deprecated("This is deprecated.")
|
||||
companion object Utils {
|
||||
private fun JadbDevice.run(
|
||||
command: String,
|
||||
with: String,
|
||||
) = run(command.applyReplacement(with))
|
||||
|
||||
private fun String.applyReplacement(with: String) = replace(PLACEHOLDER, with)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adb manager for non-rooted devices.
|
||||
*
|
||||
* @param deviceSerial The device serial. If null, the first connected device will be used.
|
||||
*/
|
||||
@Deprecated("Use AdbInstaller instead.")
|
||||
class UserAdbManager internal constructor(deviceSerial: String?) : AdbManager(deviceSerial) {
|
||||
override val installer = AdbInstaller(deviceSerial)
|
||||
|
||||
@Suppress("DeprecatedCallableAddReplaceWith")
|
||||
@Deprecated("Use AdbInstaller.install instead.")
|
||||
override fun install(apk: Apk) = suspend {
|
||||
installer.install(Installer.Apk(apk.file, apk.packageName))
|
||||
}
|
||||
|
||||
@Suppress("DeprecatedCallableAddReplaceWith")
|
||||
@Deprecated("Use AdbInstaller.uninstall instead.")
|
||||
override fun uninstall(packageName: String) = suspend {
|
||||
installer.uninstall(packageName)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apk file for [AdbManager].
|
||||
*
|
||||
* @param file The [Apk] file.
|
||||
* @param packageName The package name of the [Apk] file.
|
||||
*/
|
||||
@Deprecated("Use Installer.Apk instead.")
|
||||
class Apk(val file: File, val packageName: String? = null)
|
||||
|
||||
@Deprecated("Use AdbCommandRunner.DeviceNotFoundException instead.")
|
||||
class DeviceNotFoundException internal constructor(deviceSerial: String? = null) :
|
||||
Exception(
|
||||
deviceSerial?.let {
|
||||
"The device with the ADB device serial \"$deviceSerial\" can not be found"
|
||||
} ?: "No ADB device found",
|
||||
)
|
||||
|
||||
@Deprecated("Use RootInstaller.FailedToFindInstalledPackageException instead.")
|
||||
class FailedToFindInstalledPackageException internal constructor(packageName: String) :
|
||||
Exception("Failed to find installed package \"$packageName\" because no activity was found")
|
||||
|
||||
@Deprecated("Use RootInstaller.PackageNameRequiredException instead.")
|
||||
class PackageNameRequiredException internal constructor() :
|
||||
Exception("Package name is required")
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package app.revanced.library.installation.command
|
||||
|
||||
import app.revanced.library.installation.installer.Utils
|
||||
import se.vidstige.jadb.JadbDevice
|
||||
import se.vidstige.jadb.RemoteFile
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
|
||||
/**
|
||||
* [AdbShellCommandRunner] for running commands on a device remotely using ADB.
|
||||
*
|
||||
* @see ShellCommandRunner
|
||||
*/
|
||||
class AdbShellCommandRunner : ShellCommandRunner {
|
||||
private val device: JadbDevice
|
||||
|
||||
/**
|
||||
* Creates a [AdbShellCommandRunner] for the given device.
|
||||
*
|
||||
* @param device The device.
|
||||
*/
|
||||
internal constructor(device: JadbDevice) {
|
||||
this.device = device
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a [AdbShellCommandRunner] for the device with the given serial.
|
||||
*
|
||||
* @param deviceSerial deviceSerial The device serial. If null, the first connected device will be used.
|
||||
*/
|
||||
internal constructor(deviceSerial: String?) {
|
||||
device = Utils.getDevice(deviceSerial, logger)
|
||||
}
|
||||
|
||||
override fun runCommand(command: String) = device.shellProcessBuilder(command).start().let { process ->
|
||||
object : RunResult {
|
||||
override val exitCode by lazy { process.waitFor() }
|
||||
override val output by lazy { process.inputStream.bufferedReader().readText() }
|
||||
override val error by lazy { process.errorStream.bufferedReader().readText() }
|
||||
|
||||
override fun waitFor() {
|
||||
process.waitFor()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun hasRootPermission(): Boolean = invoke("whoami").exitCode == 0
|
||||
|
||||
override fun write(content: InputStream, targetFilePath: String) =
|
||||
device.push(content, System.currentTimeMillis(), 644, RemoteFile(targetFilePath))
|
||||
|
||||
/**
|
||||
* Moves the given [file] from the local to the target file path on the device.
|
||||
*
|
||||
* @param file The file to move.
|
||||
* @param targetFilePath The target file path.
|
||||
*/
|
||||
override fun move(file: File, targetFilePath: String) = device.push(file, RemoteFile(targetFilePath))
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package app.revanced.library.installation.command
|
||||
|
||||
/**
|
||||
* The result of a command execution.
|
||||
*/
|
||||
interface RunResult {
|
||||
/**
|
||||
* The exit code of the command.
|
||||
*/
|
||||
val exitCode: Int
|
||||
|
||||
/**
|
||||
* The output of the command.
|
||||
*/
|
||||
val output: String
|
||||
|
||||
/**
|
||||
* The error of the command.
|
||||
*/
|
||||
val error: String
|
||||
|
||||
/**
|
||||
* Waits for the command to finish.
|
||||
*/
|
||||
fun waitFor() {}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package app.revanced.library.installation.command
|
||||
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.util.logging.Logger
|
||||
|
||||
/**
|
||||
* [ShellCommandRunner] for running commands on a device.
|
||||
*/
|
||||
abstract class ShellCommandRunner internal constructor() {
|
||||
protected val logger: Logger = Logger.getLogger(this::class.java.name)
|
||||
|
||||
/**
|
||||
* Writes the given [content] to the file at the given [targetFilePath] path.
|
||||
*
|
||||
* @param content The content of the file.
|
||||
* @param targetFilePath The target file path.
|
||||
*/
|
||||
internal abstract fun write(
|
||||
content: InputStream,
|
||||
targetFilePath: String,
|
||||
)
|
||||
|
||||
/**
|
||||
* Moves the given [file] to the given [targetFilePath] path.
|
||||
*
|
||||
* @param file The file to move.
|
||||
* @param targetFilePath The target file path.
|
||||
*/
|
||||
internal abstract fun move(
|
||||
file: File,
|
||||
targetFilePath: String,
|
||||
)
|
||||
|
||||
/**
|
||||
* Runs the given [command] on the device as root.
|
||||
*
|
||||
* @param command The command to run.
|
||||
* @return The [RunResult].
|
||||
*/
|
||||
protected abstract fun runCommand(command: String): RunResult
|
||||
|
||||
/**
|
||||
* Checks if the device has root permission.
|
||||
*
|
||||
* @return True if the device has root permission, false otherwise.
|
||||
*/
|
||||
internal abstract fun hasRootPermission(): Boolean
|
||||
|
||||
/**
|
||||
* Runs a command on the device as root.
|
||||
*
|
||||
* @param command The command to run.
|
||||
* @return The [RunResult].
|
||||
*/
|
||||
internal operator fun invoke(
|
||||
command: String,
|
||||
) = runCommand("su -c \'$command\'")
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package app.revanced.library.installation.installer
|
||||
|
||||
import app.revanced.library.installation.command.AdbShellCommandRunner
|
||||
import app.revanced.library.installation.installer.Constants.INSTALLED_APK_PATH
|
||||
import app.revanced.library.installation.installer.Installer.Apk
|
||||
import se.vidstige.jadb.JadbException
|
||||
import se.vidstige.jadb.managers.Package
|
||||
import se.vidstige.jadb.managers.PackageManager
|
||||
|
||||
/**
|
||||
* [AdbInstaller] for installing and uninstalling [Apk] files using ADB.
|
||||
*
|
||||
* @param deviceSerial The device serial. If null, the first connected device will be used.
|
||||
*
|
||||
* @see Installer
|
||||
*/
|
||||
class AdbInstaller(
|
||||
deviceSerial: String? = null,
|
||||
) : Installer<AdbInstallerResult, Installation>() {
|
||||
private val device = Utils.getDevice(deviceSerial, logger)
|
||||
private val adbShellCommandRunner = AdbShellCommandRunner(device)
|
||||
private val packageManager = PackageManager(device)
|
||||
|
||||
init {
|
||||
logger.fine("Connected to $deviceSerial")
|
||||
}
|
||||
|
||||
override suspend fun install(apk: Apk): AdbInstallerResult {
|
||||
logger.info("Installing ${apk.file.name}")
|
||||
|
||||
return runPackageManager { install(apk.file) }
|
||||
}
|
||||
|
||||
override suspend fun uninstall(packageName: String): AdbInstallerResult {
|
||||
logger.info("Uninstalling $packageName")
|
||||
|
||||
return runPackageManager { uninstall(Package(packageName)) }
|
||||
}
|
||||
|
||||
override suspend fun getInstallation(packageName: String): Installation? = packageManager.packages.find {
|
||||
it.toString() == packageName
|
||||
}?.let { Installation(adbShellCommandRunner(INSTALLED_APK_PATH).output) }
|
||||
|
||||
private fun runPackageManager(block: PackageManager.() -> Unit) = try {
|
||||
packageManager.run(block)
|
||||
|
||||
AdbInstallerResult.Success
|
||||
} catch (e: JadbException) {
|
||||
AdbInstallerResult.Failure(e)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package app.revanced.library.installation.installer
|
||||
|
||||
import app.revanced.library.installation.installer.Installer.Apk
|
||||
|
||||
/**
|
||||
* The result of installing or uninstalling an [Apk] via ADB using [AdbInstaller].
|
||||
*
|
||||
* @see AdbInstaller
|
||||
*/
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
interface AdbInstallerResult {
|
||||
/**
|
||||
* The result of installing an [Apk] successfully.
|
||||
*/
|
||||
object Success : AdbInstallerResult
|
||||
|
||||
/**
|
||||
* The result of installing an [Apk] unsuccessfully.
|
||||
*
|
||||
* @param exception The exception that caused the installation to fail.
|
||||
*/
|
||||
class Failure internal constructor(val exception: Exception) : AdbInstallerResult
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package app.revanced.library.installation.installer
|
||||
|
||||
import app.revanced.library.installation.command.AdbShellCommandRunner
|
||||
import app.revanced.library.installation.installer.Installer.Apk
|
||||
import app.revanced.library.installation.installer.RootInstaller.NoRootPermissionException
|
||||
|
||||
/**
|
||||
* [AdbRootInstaller] for installing and uninstalling [Apk] files with using ADB root permissions by mounting.
|
||||
*
|
||||
* @param deviceSerial The device serial. If null, the first connected device will be used.
|
||||
*
|
||||
* @throws NoRootPermissionException If the device does not have root permission.
|
||||
*
|
||||
* @see RootInstaller
|
||||
* @see AdbShellCommandRunner
|
||||
*/
|
||||
class AdbRootInstaller(
|
||||
deviceSerial: String? = null,
|
||||
) : RootInstaller({ AdbShellCommandRunner(deviceSerial) }) {
|
||||
init {
|
||||
logger.fine("Connected to $deviceSerial")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package app.revanced.library.installation.installer
|
||||
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
internal object Constants {
|
||||
const val PLACEHOLDER = "PLACEHOLDER"
|
||||
|
||||
const val TMP_FILE_PATH = "/data/local/tmp/revanced.tmp"
|
||||
const val MOUNT_PATH = "/data/adb/revanced/"
|
||||
const val MOUNTED_APK_PATH = "$MOUNT_PATH$PLACEHOLDER.apk"
|
||||
const val MOUNT_SCRIPT_PATH = "/data/adb/service.d/mount_revanced_$PLACEHOLDER.sh"
|
||||
|
||||
const val EXISTS = "[[ -f $PLACEHOLDER ]] || exit 1"
|
||||
const val MOUNT_GREP = "grep $PLACEHOLDER /proc/mounts"
|
||||
const val DELETE = "rm -rf $PLACEHOLDER"
|
||||
const val CREATE_DIR = "mkdir -p"
|
||||
const val RESTART = "am start -S $PLACEHOLDER"
|
||||
const val KILL = "am force-stop $PLACEHOLDER"
|
||||
const val INSTALLED_APK_PATH = "pm path $PLACEHOLDER"
|
||||
const val CREATE_INSTALLATION_PATH = "$CREATE_DIR $MOUNT_PATH"
|
||||
|
||||
const val MOUNT_APK =
|
||||
"base_path=\"$MOUNTED_APK_PATH\" && " +
|
||||
"mv $TMP_FILE_PATH \$base_path && " +
|
||||
"chmod 644 \$base_path && " +
|
||||
"chown system:system \$base_path && " +
|
||||
"chcon u:object_r:apk_data_file:s0 \$base_path"
|
||||
|
||||
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"
|
||||
|
||||
const val INSTALL_MOUNT_SCRIPT = "mv $TMP_FILE_PATH $MOUNT_SCRIPT_PATH && chmod +x $MOUNT_SCRIPT_PATH"
|
||||
|
||||
val MOUNT_SCRIPT =
|
||||
"""
|
||||
#!/system/bin/sh
|
||||
until [ "$( getprop sys.boot_completed )" = 1 ]; do sleep 3; done
|
||||
until [ -d "/sdcard/Android" ]; do sleep 1; done
|
||||
|
||||
stock_path=$( pm path $PLACEHOLDER | grep base | sed 's/package://g' )
|
||||
|
||||
# Make sure the app is installed.
|
||||
if [ -z "${'$'}stock_path" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Unmount any existing installations to prevent multiple unnecessary mounts.
|
||||
$UMOUNT
|
||||
|
||||
base_path="$MOUNTED_APK_PATH"
|
||||
|
||||
chcon u:object_r:apk_data_file:s0 ${'$'}base_path
|
||||
|
||||
# Use Magisk mirror, if possible.
|
||||
if command -v magisk &> /dev/null; then
|
||||
MIRROR="${'$'}(magisk --path)/.magisk/mirror"
|
||||
fi
|
||||
|
||||
mount -o bind ${'$'}MIRROR${'$'}base_path ${'$'}stock_path
|
||||
|
||||
# Kill the app to force it to restart the mounted APK in case it's currently running.
|
||||
$KILL
|
||||
""".trimIndent()
|
||||
|
||||
/**
|
||||
* Replaces the [PLACEHOLDER] with the given [replacement].
|
||||
*
|
||||
* @param replacement The replacement to use.
|
||||
* @return The replaced string.
|
||||
*/
|
||||
operator fun String.invoke(replacement: String) = replace(PLACEHOLDER, replacement)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package app.revanced.library.installation.installer
|
||||
|
||||
/**
|
||||
* [Installation] of an apk file.
|
||||
*
|
||||
* @param apkFilePath The apk file path.
|
||||
*/
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
open class Installation internal constructor(
|
||||
val apkFilePath: String,
|
||||
)
|
||||
@@ -0,0 +1,53 @@
|
||||
package app.revanced.library.installation.installer
|
||||
|
||||
import app.revanced.library.installation.installer.Installer.Apk
|
||||
import java.io.File
|
||||
import java.util.logging.Logger
|
||||
|
||||
/**
|
||||
* [Installer] for installing and uninstalling [Apk] files.
|
||||
*
|
||||
* @param TInstallerResult The type of the result of the installation.
|
||||
* @param TInstallation The type of the installation.
|
||||
*/
|
||||
abstract class Installer<TInstallerResult, TInstallation : Installation> internal constructor() {
|
||||
/**
|
||||
* The [Logger].
|
||||
*/
|
||||
protected val logger: Logger = Logger.getLogger(this::class.java.name)
|
||||
|
||||
/**
|
||||
* Installs the [Apk] file.
|
||||
*
|
||||
* @param apk The [Apk] file.
|
||||
*
|
||||
* @return The result of the installation.
|
||||
*/
|
||||
abstract suspend fun install(apk: Apk): TInstallerResult
|
||||
|
||||
/**
|
||||
* Uninstalls the package.
|
||||
*
|
||||
* @param packageName The package name.
|
||||
*
|
||||
* @return The result of the uninstallation.
|
||||
*/
|
||||
abstract suspend fun uninstall(packageName: String): TInstallerResult
|
||||
|
||||
/**
|
||||
* Gets the current installation or null if not installed.
|
||||
*
|
||||
* @param packageName The package name.
|
||||
*
|
||||
* @return The installation.
|
||||
*/
|
||||
abstract suspend fun getInstallation(packageName: String): TInstallation?
|
||||
|
||||
/**
|
||||
* Apk file for [Installer].
|
||||
*
|
||||
* @param file The [Apk] file.
|
||||
* @param packageName The package name of the [Apk] file.
|
||||
*/
|
||||
class Apk(val file: File, val packageName: String? = null)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package app.revanced.library.installation.installer
|
||||
|
||||
/**
|
||||
* [RootInstallation] of the apk file that is mounted to [installedApkFilePath] with root permissions.
|
||||
*
|
||||
* @param installedApkFilePath The installed apk file path or null if the apk is not installed.
|
||||
* @param apkFilePath The mounting apk file path.
|
||||
* @param mounted Whether the apk is mounted to [installedApkFilePath].
|
||||
*/
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
class RootInstallation internal constructor(
|
||||
val installedApkFilePath: String?,
|
||||
apkFilePath: String,
|
||||
val mounted: Boolean,
|
||||
) : Installation(apkFilePath)
|
||||
@@ -0,0 +1,135 @@
|
||||
package app.revanced.library.installation.installer
|
||||
|
||||
import app.revanced.library.installation.command.ShellCommandRunner
|
||||
import app.revanced.library.installation.installer.Constants.CREATE_INSTALLATION_PATH
|
||||
import app.revanced.library.installation.installer.Constants.DELETE
|
||||
import app.revanced.library.installation.installer.Constants.EXISTS
|
||||
import app.revanced.library.installation.installer.Constants.INSTALLED_APK_PATH
|
||||
import app.revanced.library.installation.installer.Constants.INSTALL_MOUNT_SCRIPT
|
||||
import app.revanced.library.installation.installer.Constants.KILL
|
||||
import app.revanced.library.installation.installer.Constants.MOUNTED_APK_PATH
|
||||
import app.revanced.library.installation.installer.Constants.MOUNT_APK
|
||||
import app.revanced.library.installation.installer.Constants.MOUNT_GREP
|
||||
import app.revanced.library.installation.installer.Constants.MOUNT_SCRIPT
|
||||
import app.revanced.library.installation.installer.Constants.MOUNT_SCRIPT_PATH
|
||||
import app.revanced.library.installation.installer.Constants.RESTART
|
||||
import app.revanced.library.installation.installer.Constants.TMP_FILE_PATH
|
||||
import app.revanced.library.installation.installer.Constants.UMOUNT
|
||||
import app.revanced.library.installation.installer.Constants.invoke
|
||||
import app.revanced.library.installation.installer.Installer.Apk
|
||||
import app.revanced.library.installation.installer.RootInstaller.NoRootPermissionException
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* [RootInstaller] for installing and uninstalling [Apk] files using root permissions by mounting.
|
||||
*
|
||||
* @param shellCommandRunnerSupplier A supplier for the [ShellCommandRunner] to use.
|
||||
*
|
||||
* @throws NoRootPermissionException If the device does not have root permission.
|
||||
*/
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
abstract class RootInstaller internal constructor(
|
||||
shellCommandRunnerSupplier: (RootInstaller) -> ShellCommandRunner,
|
||||
) : Installer<RootInstallerResult, RootInstallation>() {
|
||||
|
||||
/**
|
||||
* The command runner used to run commands on the device.
|
||||
*/
|
||||
@Suppress("LeakingThis")
|
||||
protected val shellCommandRunner = shellCommandRunnerSupplier(this)
|
||||
|
||||
init {
|
||||
if (!shellCommandRunner.hasRootPermission()) throw NoRootPermissionException()
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs the given [apk] by mounting.
|
||||
*
|
||||
* @param apk The [Apk] to install.
|
||||
*
|
||||
* @throws PackageNameRequiredException If the [Apk] does not have a package name.
|
||||
*/
|
||||
override suspend fun install(apk: Apk): RootInstallerResult {
|
||||
logger.info("Installing ${apk.packageName} by mounting")
|
||||
|
||||
val packageName = apk.packageName?.also { it.assertInstalled() } ?: throw PackageNameRequiredException()
|
||||
|
||||
// Setup files.
|
||||
apk.file.move(TMP_FILE_PATH)
|
||||
CREATE_INSTALLATION_PATH().waitFor()
|
||||
MOUNT_APK(packageName)().waitFor()
|
||||
|
||||
// Install and run.
|
||||
TMP_FILE_PATH.write(MOUNT_SCRIPT(packageName))
|
||||
INSTALL_MOUNT_SCRIPT(packageName)().waitFor()
|
||||
MOUNT_SCRIPT_PATH(packageName)().waitFor()
|
||||
RESTART(packageName)()
|
||||
|
||||
DELETE(TMP_FILE_PATH)()
|
||||
|
||||
return RootInstallerResult.SUCCESS
|
||||
}
|
||||
|
||||
override suspend fun uninstall(packageName: String): RootInstallerResult {
|
||||
logger.info("Uninstalling $packageName by unmounting")
|
||||
|
||||
UMOUNT(packageName)()
|
||||
|
||||
DELETE(MOUNTED_APK_PATH)(packageName)()
|
||||
DELETE(MOUNT_SCRIPT_PATH)(packageName)()
|
||||
DELETE(TMP_FILE_PATH)() // Remove residual.
|
||||
|
||||
KILL(packageName)()
|
||||
|
||||
return RootInstallerResult.SUCCESS
|
||||
}
|
||||
|
||||
override suspend fun getInstallation(packageName: String): RootInstallation? {
|
||||
val patchedApkPath = MOUNTED_APK_PATH(packageName)
|
||||
|
||||
val patchedApkExists = EXISTS(patchedApkPath)().exitCode == 0
|
||||
if (patchedApkExists) return null
|
||||
|
||||
return RootInstallation(
|
||||
INSTALLED_APK_PATH(packageName)().output.ifEmpty { null },
|
||||
patchedApkPath,
|
||||
MOUNT_GREP(patchedApkPath)().exitCode == 0,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs a command on the device.
|
||||
*/
|
||||
protected operator fun String.invoke() = shellCommandRunner(this)
|
||||
|
||||
/**
|
||||
* Moves the given file to the given [targetFilePath].
|
||||
*
|
||||
* @param targetFilePath The target file path.
|
||||
*/
|
||||
protected fun File.move(targetFilePath: String) = shellCommandRunner.move(this, targetFilePath)
|
||||
|
||||
/**
|
||||
* Writes the given [content] to the file.
|
||||
*
|
||||
* @param content The content of the file.
|
||||
*/
|
||||
protected fun String.write(content: String) = shellCommandRunner.write(content.byteInputStream(), this)
|
||||
|
||||
/**
|
||||
* Asserts that the package is installed.
|
||||
*
|
||||
* @throws FailedToFindInstalledPackageException If the package is not installed.
|
||||
*/
|
||||
private fun String.assertInstalled() {
|
||||
if (INSTALLED_APK_PATH(this)().output.isNotEmpty()) {
|
||||
throw FailedToFindInstalledPackageException(this)
|
||||
}
|
||||
}
|
||||
|
||||
internal class FailedToFindInstalledPackageException internal constructor(packageName: String) :
|
||||
Exception("Failed to find installed package \"$packageName\" because no activity was found")
|
||||
|
||||
internal class PackageNameRequiredException internal constructor() : Exception("Package name is required")
|
||||
internal class NoRootPermissionException internal constructor() : Exception("No root permission")
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package app.revanced.library.installation.installer
|
||||
|
||||
import app.revanced.library.installation.installer.Installer.Apk
|
||||
|
||||
/**
|
||||
* The result of installing or uninstalling an [Apk] with root permissions using [RootInstaller].
|
||||
*
|
||||
* @see RootInstaller
|
||||
*/
|
||||
enum class RootInstallerResult {
|
||||
/**
|
||||
* The result of installing an [Apk] successfully.
|
||||
*/
|
||||
SUCCESS,
|
||||
|
||||
/**
|
||||
* The result of installing an [Apk] unsuccessfully.
|
||||
*/
|
||||
FAILURE,
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package app.revanced.library.installation.installer
|
||||
|
||||
import se.vidstige.jadb.JadbConnection
|
||||
import java.util.logging.Logger
|
||||
|
||||
/**
|
||||
* Utility functions for [Installer].
|
||||
*
|
||||
* @see Installer
|
||||
*/
|
||||
internal object Utils {
|
||||
/**
|
||||
* Gets the device with the given serial.
|
||||
*
|
||||
* @param deviceSerial The device serial. If null, the first connected device will be used.
|
||||
* @param logger The logger.
|
||||
* @return The device.
|
||||
* @throws DeviceNotFoundException If no device with the given serial is found.
|
||||
*/
|
||||
internal fun getDevice(
|
||||
deviceSerial: String? = null,
|
||||
logger: Logger,
|
||||
) = with(JadbConnection().devices) {
|
||||
if (isEmpty()) throw DeviceNotFoundException()
|
||||
|
||||
deviceSerial?.let {
|
||||
firstOrNull { it.serial == deviceSerial } ?: throw DeviceNotFoundException(
|
||||
deviceSerial,
|
||||
)
|
||||
} ?: first().also {
|
||||
logger.warning("No device serial supplied. Using device with serial ${it.serial}")
|
||||
}
|
||||
}!!
|
||||
|
||||
class DeviceNotFoundException internal constructor(deviceSerial: String? = null) : Exception(
|
||||
deviceSerial?.let {
|
||||
"The device with the ADB device serial \"$deviceSerial\" can not be found"
|
||||
} ?: "No ADB device found",
|
||||
)
|
||||
}
|
||||
99
src/commonMain/kotlin/app/revanced/library/logging/Logger.kt
Normal file
99
src/commonMain/kotlin/app/revanced/library/logging/Logger.kt
Normal file
@@ -0,0 +1,99 @@
|
||||
package app.revanced.library.logging
|
||||
|
||||
import java.util.logging.Handler
|
||||
import java.util.logging.Level
|
||||
import java.util.logging.LogRecord
|
||||
import java.util.logging.SimpleFormatter
|
||||
|
||||
@Suppress("MemberVisibilityCanBePrivate", "unused")
|
||||
object Logger {
|
||||
/**
|
||||
* Rules for allowed loggers.
|
||||
*/
|
||||
private val allowedLoggersRules =
|
||||
arrayOf<String.() -> Boolean>(
|
||||
// ReVanced loggers.
|
||||
{ startsWith("app.revanced") },
|
||||
// Logs warnings when compiling resources (Logger in class brut.util.OS).
|
||||
{ this == "" },
|
||||
)
|
||||
|
||||
private val rootLogger = java.util.logging.Logger.getLogger("")
|
||||
|
||||
/**
|
||||
* Sets the format for the logger.
|
||||
*
|
||||
* @param format The format to use.
|
||||
*/
|
||||
fun setFormat(format: String = "%4\$s: %5\$s %n") {
|
||||
System.setProperty("java.util.logging.SimpleFormatter.format", format)
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all handlers from the logger.
|
||||
*/
|
||||
fun removeAllHandlers() {
|
||||
rootLogger.let { logger ->
|
||||
logger.handlers.forEach { handler ->
|
||||
handler.close()
|
||||
logger.removeHandler(handler)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a handler to the logger.
|
||||
*
|
||||
* @param publishHandler The handler for publishing the log.
|
||||
* @param flushHandler The handler for flushing the log.
|
||||
* @param closeHandler The handler for closing the log.
|
||||
*/
|
||||
fun addHandler(
|
||||
publishHandler: (log: String, level: Level, loggerName: String?) -> Unit,
|
||||
flushHandler: () -> Unit,
|
||||
closeHandler: () -> Unit,
|
||||
) = object : Handler() {
|
||||
override fun publish(record: LogRecord) =
|
||||
publishHandler(
|
||||
formatter.format(record),
|
||||
record.level,
|
||||
record.loggerName,
|
||||
)
|
||||
|
||||
override fun flush() = flushHandler()
|
||||
|
||||
override fun close() = closeHandler()
|
||||
}.also {
|
||||
it.level = Level.ALL
|
||||
it.formatter = SimpleFormatter()
|
||||
}.let(rootLogger::addHandler)
|
||||
|
||||
/**
|
||||
* Log to "standard" (error) output streams.
|
||||
*/
|
||||
fun setDefault() {
|
||||
setFormat()
|
||||
removeAllHandlers()
|
||||
|
||||
val publishHandler = handler@{ log: String, level: Level, loggerName: String? ->
|
||||
loggerName?.let { name ->
|
||||
if (allowedLoggersRules.none { it(name) }) return@handler
|
||||
}
|
||||
|
||||
log.toByteArray().let {
|
||||
if (level.intValue() > Level.WARNING.intValue()) {
|
||||
System.err.write(it)
|
||||
} else {
|
||||
System.out.write(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val flushHandler = {
|
||||
System.out.flush()
|
||||
System.err.flush()
|
||||
}
|
||||
|
||||
addHandler(publishHandler, flushHandler, flushHandler)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user