Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion api/android/revanced-library.api
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ public final class app/revanced/library/ApkSigner$Signer {
public final class app/revanced/library/ApkUtils {
public static final field INSTANCE Lapp/revanced/library/ApkUtils;
public final fun applyTo (Lapp/revanced/patcher/PatchesResult;Ljava/io/File;)V
public final fun applyToSplits (Lapp/revanced/patcher/PatchesResult;Ljava/util/Map;)V
public final fun signApk (Ljava/io/File;Ljava/io/File;Ljava/lang/String;Lapp/revanced/library/ApkUtils$KeyStoreDetails;)V
public final fun signApks (Ljava/util/Collection;Ljava/lang/String;Lapp/revanced/library/ApkUtils$KeyStoreDetails;)V
}

public final class app/revanced/library/ApkUtils$KeyStoreDetails {
Expand Down Expand Up @@ -165,8 +167,10 @@ public abstract class app/revanced/library/installation/installer/Installer {
public final class app/revanced/library/installation/installer/Installer$Apk {
public fun <init> (Ljava/io/File;Ljava/lang/String;)V
public synthetic fun <init> (Ljava/io/File;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> (Ljava/io/File;Ljava/lang/String;Ljava/util/Map;)V
public final fun getFile ()Ljava/io/File;
public final fun getPackageName ()Ljava/lang/String;
public final fun getSplitFiles ()Ljava/util/Map;
}

public final class app/revanced/library/installation/installer/LocalInstaller : app/revanced/library/installation/installer/Installer, java/io/Closeable {
Expand Down Expand Up @@ -230,4 +234,3 @@ public final class app/revanced/library/logging/Logger {
public final fun setFormat (Ljava/lang/String;)V
public static synthetic fun setFormat$default (Lapp/revanced/library/logging/Logger;Ljava/lang/String;ILjava/lang/Object;)V
}

5 changes: 4 additions & 1 deletion api/jvm/revanced-library.api
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ public final class app/revanced/library/ApkSigner$Signer {
public final class app/revanced/library/ApkUtils {
public static final field INSTANCE Lapp/revanced/library/ApkUtils;
public final fun applyTo (Lapp/revanced/patcher/PatchesResult;Ljava/io/File;)V
public final fun applyToSplits (Lapp/revanced/patcher/PatchesResult;Ljava/util/Map;)V
public final fun signApk (Ljava/io/File;Ljava/io/File;Ljava/lang/String;Lapp/revanced/library/ApkUtils$KeyStoreDetails;)V
public final fun signApks (Ljava/util/Collection;Ljava/lang/String;Lapp/revanced/library/ApkUtils$KeyStoreDetails;)V
}

public final class app/revanced/library/ApkUtils$KeyStoreDetails {
Expand Down Expand Up @@ -141,8 +143,10 @@ public abstract class app/revanced/library/installation/installer/Installer {
public final class app/revanced/library/installation/installer/Installer$Apk {
public fun <init> (Ljava/io/File;Ljava/lang/String;)V
public synthetic fun <init> (Ljava/io/File;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> (Ljava/io/File;Ljava/lang/String;Ljava/util/Map;)V
public final fun getFile ()Ljava/io/File;
public final fun getPackageName ()Ljava/lang/String;
public final fun getSplitFiles ()Ljava/util/Map;
}

public final class app/revanced/library/installation/installer/RootInstallation : app/revanced/library/installation/installer/Installation {
Expand Down Expand Up @@ -176,4 +180,3 @@ public final class app/revanced/library/logging/Logger {
public final fun setFormat (Ljava/lang/String;)V
public static synthetic fun setFormat$default (Lapp/revanced/library/logging/Logger;Ljava/lang/String;ILjava/lang/Object;)V
}

Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import android.content.pm.PackageInstaller
import android.content.pm.PackageManager
import android.os.Build
import androidx.core.content.ContextCompat
import app.revanced.library.installation.installer.Constants.splitFileName
import app.revanced.library.installation.installer.Installer.Apk
import java.io.Closeable
import java.io.File
Expand Down Expand Up @@ -57,12 +58,15 @@ class LocalInstaller(
}

override suspend fun install(apk: Apk) {
logger.info("Installing ${apk.file.name}")
logger.info("Installing ${apk.file.name} with ${apk.splitFiles.size} split APK(s)")

val packageInstaller = context.packageManager.packageInstaller

packageInstaller.openSession(packageInstaller.createSession(sessionParams)).use { session ->
session.writeApk(apk.file)
session.writeApk("base.apk", apk.file)
apk.splitFiles.forEach { (splitName, splitFile) ->
session.writeApk(splitFileName(splitName), splitFile)
}
session.commit(intentSender)
}
}
Expand Down Expand Up @@ -96,9 +100,9 @@ class LocalInstaller(
setInstallReason(PackageManager.INSTALL_REASON_USER)
}

private fun PackageInstaller.Session.writeApk(apk: File) {
private fun PackageInstaller.Session.writeApk(name: String, apk: File) {
apk.inputStream().use { inputStream ->
openWrite(apk.name, 0, apk.length()).use { outputStream ->
openWrite(name, 0, apk.length()).use { outputStream ->
inputStream.copyTo(outputStream, 1024 * 1024)
fsync(outputStream)
}
Expand Down
135 changes: 97 additions & 38 deletions library/src/commonMain/kotlin/app/revanced/library/ApkUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ 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.nio.file.Files
import java.nio.file.StandardCopyOption
import java.util.*
import java.util.logging.Logger
import kotlin.time.Duration.Companion.days
Expand Down Expand Up @@ -37,6 +39,52 @@ object ApkUtils {
),
)

private fun getSigner(
signer: String,
keyStoreDetails: KeyStoreDetails,
) = ApkSigner.newApkSigner(
signer,
if (keyStoreDetails.keyStore.exists()) {
readPrivateKeyCertificatePairFromKeyStore(keyStoreDetails)
} else {
newPrivateKeyCertificatePair(PrivateKeyCertificatePairDetails(), keyStoreDetails)
},
)

/**
* Applies [PatchedResources] to the given [ZFile].
*/
private fun applyResources(targetApkZFile: ZFile, resources: PatchesResult.PatchedResources) {
// 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 ->
entry.centralDirectoryHeader.name in resources.deleteResources
}.forEach(StoredEntry::delete)
}
}

/**
* Applies the [PatchesResult] to the given [apkFile].
*
Expand All @@ -57,36 +105,7 @@ object ApkUtils {
dexFile.stream.close()
}

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 ->
entry.centralDirectoryHeader.name in resources.deleteResources
}.forEach(StoredEntry::delete)
}
}
resources?.let { applyResources(targetApkZFile, it) }

logger.info("Aligning APK")

Expand All @@ -96,6 +115,28 @@ object ApkUtils {
}
}

/**
* Applies the [PatchesResult] split resources to the given split APK files.
*
* @param splitApkFiles The split APK files keyed by split name.
*/
fun PatchesResult.applyToSplits(splitApkFiles: Map<String, File>) {
splitResources.forEach { (splitName, resources) ->
val splitFile = splitApkFiles[splitName]
?: error("No split APK file found for split \"$splitName\"")

ZFile.openReadWrite(splitFile, zFileOptions).use { targetApkZFile ->
applyResources(targetApkZFile, resources)

logger.info("Aligning split APK \"$splitName\"")

targetApkZFile.realign()

logger.fine("Writing changes for split \"$splitName\"")
}
}
}

/**
* Creates a new private key and certificate pair and saves it to the keystore in [keyStoreDetails].
*
Expand Down Expand Up @@ -158,14 +199,32 @@ object ApkUtils {
outputApkFile: File,
signer: String,
keyStoreDetails: KeyStoreDetails,
) = ApkSigner.newApkSigner(
signer,
if (keyStoreDetails.keyStore.exists()) {
readPrivateKeyCertificatePairFromKeyStore(keyStoreDetails)
} else {
newPrivateKeyCertificatePair(PrivateKeyCertificatePairDetails(), keyStoreDetails)
},
).signApk(inputApkFile, outputApkFile)
) = getSigner(signer, keyStoreDetails).signApk(inputApkFile, outputApkFile)

/**
* Signs multiple APK files in place using the same signer.
*
* @param apkFiles The APK files to sign.
* @param signer The name of the signer.
* @param keyStoreDetails The details for the keystore.
*/
fun signApks(
apkFiles: Collection<File>,
signer: String,
keyStoreDetails: KeyStoreDetails,
) {
if (apkFiles.isEmpty()) return

val apkSigner = getSigner(signer, keyStoreDetails)

apkFiles.forEach { apkFile ->
val signedOutputFile = File(apkFile.parentFile, "${apkFile.nameWithoutExtension}-signed.${apkFile.extension}")

apkSigner.signApk(apkFile, signedOutputFile)

Files.move(signedOutputFile.toPath(), apkFile.toPath(), StandardCopyOption.REPLACE_EXISTING)
}
}

/**
* Details for a keystore.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,17 @@ import app.revanced.library.installation.command.AdbShellCommandRunner
import app.revanced.library.installation.command.ShellCommandRunner
import app.revanced.library.installation.installer.Constants.GET_SDK_VERSION
import app.revanced.library.installation.installer.Constants.INSTALLED_APK_PATH
import app.revanced.library.installation.installer.Constants.TMP_FILE_PATH
import app.revanced.library.installation.installer.Constants.invoke
import app.revanced.library.installation.installer.Constants.splitFileName
import se.vidstige.jadb.JadbDevice
import se.vidstige.jadb.JadbException
import se.vidstige.jadb.RemoteFile
import java.io.IOException
import se.vidstige.jadb.managers.Package
import se.vidstige.jadb.managers.PackageManager
import se.vidstige.jadb.managers.PackageManager.UPDATE_OWNERSHIP
import java.nio.charset.StandardCharsets

/**
* [AdbInstaller] for installing and uninstalling [Apk] files using ADB.
Expand All @@ -19,11 +26,12 @@ import se.vidstige.jadb.managers.PackageManager.UPDATE_OWNERSHIP
class AdbInstaller(
deviceSerial: String? = null,
) : Installer<AdbInstallerResult, Installation>() {
private val device: JadbDevice
private val shellCommandRunner: ShellCommandRunner
private val packageManager: PackageManager

init {
val device = getDevice(deviceSerial, logger)
device = getDevice(deviceSerial, logger)
shellCommandRunner = AdbShellCommandRunner(device)
packageManager = PackageManager(device)

Expand All @@ -33,8 +41,14 @@ class AdbInstaller(
override suspend fun install(apk: Apk): AdbInstallerResult {
return runPackageManager {
val sdkVersion = shellCommandRunner(GET_SDK_VERSION).output.toInt()
if (sdkVersion < 34) install(apk.file)
else installWithOptions(apk.file, listOf(UPDATE_OWNERSHIP))
if (apk.splitFiles.isEmpty()) {
if (sdkVersion < 34) install(apk.file)
else installWithOptions(apk.file, listOf(UPDATE_OWNERSHIP))

return@runPackageManager
}

installSplitPackage(apk, sdkVersion >= 34)
}
}

Expand All @@ -48,11 +62,50 @@ class AdbInstaller(
it.toString() == packageName
}?.let { Installation(shellCommandRunner(INSTALLED_APK_PATH).output) }

private fun installSplitPackage(apk: Apk, updateOwnership: Boolean) {
logger.info("Installing ${apk.file.name} with ${apk.splitFiles.size} split APK(s)")

val remoteFiles = buildMap {
put(apk.file, RemoteFile(TMP_FILE_PATH("base.apk")))
apk.splitFiles.forEach { (splitName, splitFile) ->
put(splitFile, RemoteFile(TMP_FILE_PATH(splitFileName(splitName))))
}
}

try {
remoteFiles.forEach { (file, remoteFile) ->
device.push(file, remoteFile)
}

val arguments = mutableListOf("install-multiple")
if (updateOwnership) {
arguments += "--update-ownership"
}
arguments += remoteFiles.values.map(RemoteFile::getPath)

val result = device.executeShell("pm", *arguments.toTypedArray()).use { inputStream ->
inputStream.readBytes().toString(StandardCharsets.UTF_8)
}

if (!result.contains("Success")) {
throw JadbException("Could not install ${apk.file.name}: $result")
}
} finally {
remoteFiles.values.forEach(::removeRemoteFile)
}
}

private fun removeRemoteFile(remoteFile: RemoteFile) {
device.executeShell("rm", "-f", remoteFile.path).use { }
}

private fun runPackageManager(block: PackageManager.() -> Unit) = try {
packageManager.run(block)

AdbInstallerResult.Success
} catch (e: JadbException) {
AdbInstallerResult.Failure(e)
} catch (e: IOException) {
AdbInstallerResult.Failure(e)
}
}
Loading