diff --git a/patcher/api/jvm/patcher.api b/patcher/api/jvm/patcher.api index ae6f4569..da65d969 100644 --- a/patcher/api/jvm/patcher.api +++ b/patcher/api/jvm/patcher.api @@ -327,6 +327,20 @@ public final class app/revanced/com/android/tools/smali/dexlib2/mutable/MutableM public final fun toMutable (Lcom/android/tools/smali/dexlib2/iface/MethodParameter;)Lapp/revanced/com/android/tools/smali/dexlib2/mutable/MutableMethodParameter; } +public abstract class app/revanced/patcher/Apk { + public synthetic fun (Ljava/io/File;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getFile ()Ljava/io/File; +} + +public final class app/revanced/patcher/Apk$Single : app/revanced/patcher/Apk { + public fun (Ljava/io/File;)V +} + +public final class app/revanced/patcher/Apk$Split : app/revanced/patcher/Apk { + public fun (Ljava/io/File;Ljava/util/Map;)V + public final fun getSplitApkFiles ()Ljava/util/Map; +} + public final class app/revanced/patcher/ClassDefComposing { public static final field INSTANCE Lapp/revanced/patcher/ClassDefComposing; public final fun composingFirstMethod ([Ljava/lang/String;Lkotlin/jvm/functions/Function3;)Lkotlin/properties/ReadOnlyProperty; @@ -781,6 +795,7 @@ public final class app/revanced/patcher/MutablePredicateList : java/util/List, k public final class app/revanced/patcher/PatchesResult { public final fun getDexFiles ()Ljava/util/Set; public final fun getResources ()Lapp/revanced/patcher/PatchesResult$PatchedResources; + public final fun getSplitResources ()Ljava/util/Map; } public final class app/revanced/patcher/PatchesResult$PatchedDexFile { @@ -797,7 +812,9 @@ public final class app/revanced/patcher/PatchesResult$PatchedResources { public final class app/revanced/patcher/PatchingKt { public static final fun apply (Ljava/util/Set;Lapp/revanced/patcher/patch/BytecodePatchContext;Lapp/revanced/patcher/patch/ResourcePatchContext;Lkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/PatchesResult; + public static final fun patcher (Lapp/revanced/patcher/Apk;Ljava/io/File;Ljava/io/File;Ljava/lang/String;Lkotlin/jvm/functions/Function2;)Lkotlin/jvm/functions/Function1; public static final fun patcher (Ljava/io/File;Ljava/io/File;Ljava/io/File;Ljava/lang/String;Lkotlin/jvm/functions/Function2;)Lkotlin/jvm/functions/Function1; + public static synthetic fun patcher$default (Lapp/revanced/patcher/Apk;Ljava/io/File;Ljava/io/File;Ljava/lang/String;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lkotlin/jvm/functions/Function1; public static synthetic fun patcher$default (Ljava/io/File;Ljava/io/File;Ljava/io/File;Ljava/lang/String;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lkotlin/jvm/functions/Function1; } @@ -1209,12 +1226,16 @@ public class app/revanced/patcher/patch/ResourcePatchBuilder : app/revanced/patc public final class app/revanced/patcher/patch/ResourcePatchContext : app/revanced/patcher/patch/PatchContext { public final fun delete (Ljava/lang/String;)Z + public final fun deleteSplit (Ljava/lang/String;Ljava/lang/String;)V public final fun document (Ljava/io/InputStream;)Lapp/revanced/patcher/util/Document; public final fun document (Ljava/lang/String;)Lapp/revanced/patcher/util/Document; public fun get ()Lapp/revanced/patcher/PatchesResult$PatchedResources; public synthetic fun get ()Ljava/lang/Object; public final fun get (Ljava/lang/String;Z)Ljava/io/File; public static synthetic fun get$default (Lapp/revanced/patcher/patch/ResourcePatchContext;Ljava/lang/String;ZILjava/lang/Object;)Ljava/io/File; + public final fun split (Ljava/lang/String;Ljava/lang/String;Z)Ljava/io/File; + public static synthetic fun split$default (Lapp/revanced/patcher/patch/ResourcePatchContext;Ljava/lang/String;Ljava/lang/String;ZILjava/lang/Object;)Ljava/io/File; + public final fun splitDocument (Ljava/lang/String;Ljava/lang/String;)Lapp/revanced/patcher/util/Document; } public final class app/revanced/patcher/util/Document : java/io/Closeable, org/w3c/dom/Document { diff --git a/patcher/src/commonMain/kotlin/app/revanced/patcher/Apk.kt b/patcher/src/commonMain/kotlin/app/revanced/patcher/Apk.kt new file mode 100644 index 00000000..b0c51f7a --- /dev/null +++ b/patcher/src/commonMain/kotlin/app/revanced/patcher/Apk.kt @@ -0,0 +1,28 @@ +package app.revanced.patcher + +import java.io.File + +/** + * An APK to patch. + * + * @param file The base APK file. This is the source of truth for the manifest, package metadata, and primary dex files. + */ +sealed class Apk(val file: File) { + /** + * A single monolithic APK. + * + * @param file The APK file. + */ + class Single(file: File) : Apk(file) + + /** + * A split APK bundle. + * + * @param baseApk The base APK file. + * @param splitApkFiles The split APK files, keyed by split name. + */ + class Split( + baseApk: File, + val splitApkFiles: Map, + ) : Apk(baseApk) +} diff --git a/patcher/src/commonMain/kotlin/app/revanced/patcher/Patching.kt b/patcher/src/commonMain/kotlin/app/revanced/patcher/Patching.kt index c099a193..9f4027a4 100644 --- a/patcher/src/commonMain/kotlin/app/revanced/patcher/Patching.kt +++ b/patcher/src/commonMain/kotlin/app/revanced/patcher/Patching.kt @@ -13,6 +13,14 @@ fun patcher( aaptBinaryPath: File? = null, frameworkFileDirectory: String? = null, getPatches: (packageName: String, versionName: String) -> Set, +) = patcher(Apk.Single(apkFile), temporaryFilesPath, aaptBinaryPath, frameworkFileDirectory, getPatches) + +fun patcher( + apk: Apk, + temporaryFilesPath: File = File("revanced-patcher-temporary-files"), + aaptBinaryPath: File? = null, + frameworkFileDirectory: String? = null, + getPatches: (packageName: String, versionName: String) -> Set, ): (emit: (PatchResult) -> Unit) -> PatchesResult { val logger = Logger.getLogger("Patcher") @@ -25,12 +33,14 @@ fun patcher( } val apkFilesPath = temporaryFilesPath.kmpResolve("apk").also { it.mkdirs() } + val splitApkFilesPath = temporaryFilesPath.kmpResolve("splits").also { it.mkdirs() } val patchedFilesPath = temporaryFilesPath.kmpResolve("patched").also { it.mkdirs() } val resourcePatchContext = ResourcePatchContext( - apkFile, + apk, apkFilesPath, + splitApkFilesPath, patchedFilesPath, aaptBinaryPath, frameworkFileDirectory, @@ -45,7 +55,7 @@ fun patcher( // After initializing the resource context, to keep memory usage time low. val bytecodePatchContext = BytecodePatchContext( - apkFile, + apk, patchedFilesPath, ) @@ -121,18 +131,22 @@ fun Set.apply( ) } - return PatchesResult(bytecodePatchContext.get(), resourcePatchContext.get()) + val (baseResources, splitResources) = resourcePatchContext.getCompiledResources() + + return PatchesResult(bytecodePatchContext.get(), baseResources, splitResources) } /** * The result of applying patches. * * @param dexFiles The patched dex files. - * @param resources The patched resources. + * @param resources The patched base APK resources. + * @param splitResources The patched resources for each split APK, keyed by split name. */ class PatchesResult internal constructor( val dexFiles: Set, val resources: PatchedResources?, + val splitResources: Map = emptyMap(), ) { /** * A dex file. diff --git a/patcher/src/commonMain/kotlin/app/revanced/patcher/patch/BytecodePatchContext.kt b/patcher/src/commonMain/kotlin/app/revanced/patcher/patch/BytecodePatchContext.kt index c2e3142f..77cd9625 100644 --- a/patcher/src/commonMain/kotlin/app/revanced/patcher/patch/BytecodePatchContext.kt +++ b/patcher/src/commonMain/kotlin/app/revanced/patcher/patch/BytecodePatchContext.kt @@ -5,6 +5,7 @@ import app.revanced.com.android.tools.smali.dexlib2.mutable.MutableClassDef.Comp import app.revanced.java.io.kmpDeleteRecursively import app.revanced.java.io.kmpInputStream import app.revanced.java.io.kmpResolve +import app.revanced.patcher.Apk import app.revanced.patcher.PatchesResult import app.revanced.patcher.extensions.instructionsOrNull import app.revanced.patcher.extensions.string @@ -27,12 +28,12 @@ import kotlin.reflect.jvm.jvmName /** * A context for patches containing the current state of the bytecode. * - * @param apkFile The apk [File] to patch. + * @param apk The [Apk] to patch. * @param patchedFilesPath The path to the temporary apk files directory. */ @Suppress("MemberVisibilityCanBePrivate") class BytecodePatchContext internal constructor( - internal val apkFile: File, + internal val apk: Apk, internal val patchedFilesPath: File, ) : PatchContext> { private val logger = Logger.getLogger(this::class.jvmName) @@ -56,15 +57,7 @@ class BytecodePatchContext internal constructor( // private val _methodsWithString = methodsByString.values.flatten().toMutableSet() // val methodsWithString: Set = _methodsWithString - constructor() : this( - MultiDexIO.readDexFile( - true, - apkFile, - BasicDexFileNamer(), - null, - null, - ), - ) + constructor() : this(readDexFiles(apk)) internal val opcodes = dexFile.opcodes @@ -287,3 +280,38 @@ class BytecodePatchContext internal constructor( return patchedDexFileResults } } + +private fun readDexFiles(apk: Apk): DexFile { + val baseDex = MultiDexIO.readDexFile( + true, + apk.file, + BasicDexFileNamer(), + null, + null, + ) + + if (apk !is Apk.Split) return baseDex + + val allClasses = baseDex.classes.toMutableSet() + + for ((_, splitFile) in apk.splitApkFiles) { + val splitDex = try { + MultiDexIO.readDexFile( + true, + splitFile, + BasicDexFileNamer(), + null, + null, + ) + } catch (_: lanchon.multidexlib2.EmptyMultiDexContainerException) { + // Config splits (ABI, density, language) contain no dex files. + continue + } + allClasses += splitDex.classes + } + + return object : DexFile { + override fun getClasses() = allClasses + override fun getOpcodes() = baseDex.opcodes + } +} diff --git a/patcher/src/commonMain/kotlin/app/revanced/patcher/patch/ResourcePatchContext.kt b/patcher/src/commonMain/kotlin/app/revanced/patcher/patch/ResourcePatchContext.kt index 57ea349f..b3bf668f 100644 --- a/patcher/src/commonMain/kotlin/app/revanced/patcher/patch/ResourcePatchContext.kt +++ b/patcher/src/commonMain/kotlin/app/revanced/patcher/patch/ResourcePatchContext.kt @@ -1,6 +1,7 @@ package app.revanced.patcher.patch import app.revanced.java.io.kmpResolve +import app.revanced.patcher.Apk import app.revanced.patcher.PatchesResult import app.revanced.patcher.patch.ResourcePatchContext.ResourceDecodingMode.ALL import app.revanced.patcher.util.Document @@ -25,20 +26,22 @@ import kotlin.reflect.jvm.jvmName /** * A context for patches containing the current state of resources. * - * @param apkFile The apk file to patch. - * @param apkFilesPath The path to the temporary apk files directory. + * @param apk The [Apk] to patch. + * @param apkFilesPath The path to the temporary base APK files directory. + * @param splitApkFilesPath The path to the temporary split APK files directory. * @param patchedFilesPath The path to the temporary patched files directory. * @param aaptBinaryPath The path to a custom aapt binary. * @param frameworkFileDirectory The path to the directory to cache the framework file in. */ class ResourcePatchContext internal constructor( - private val apkFile: File, + private val apk: Apk, private val apkFilesPath: File, + private val splitApkFilesPath: File, private val patchedFilesPath: File, aaptBinaryPath: File? = null, frameworkFileDirectory: String? = null, ) : PatchContext { - private val apkInfo = ApkInfo(ExtFile(apkFile)) + private val apkInfo = ApkInfo(ExtFile(apk.file)) private val logger = Logger.getLogger(ResourcePatchContext::class.jvmName) @@ -50,21 +53,36 @@ class ResourcePatchContext internal constructor( private var decodingMode = ResourceDecodingMode.MANIFEST + private val splitApkInfos: Map = + if (apk is Apk.Split) { + apk.splitApkFiles.mapValues { (_, file) -> ApkInfo(ExtFile(file)) } + } else { + emptyMap() + } + + private val splitDecodingModes: MutableMap = + splitApkInfos.keys.associateWithTo(mutableMapOf()) { ResourceDecodingMode.MANIFEST } + /** * Read a document from an [InputStream]. */ fun document(inputStream: InputStream) = Document(inputStream) /** - * Read and write documents in the [apkFile]. + * Read and write documents in the base APK. */ fun document(path: String) = Document(get(path)) /** - * Set of resources from [apkFile] to delete. + * Set of resources from the base APK to delete. */ private val deleteResources = mutableSetOf() + /** + * Per-split sets of resources to delete. + */ + private val splitDeleteResources: MutableMap> = mutableMapOf() + internal fun decodeManifest(): Pair { logger.info("Decoding manifest") @@ -111,6 +129,14 @@ class ResourcePatchContext internal constructor( val resourcesDecoder = ResourcesDecoder(resourceConfig, apkInfo).also { + it.loadAuxiliaryPkgs( + splitApkInfos.values + .asSequence() + .filter { splitApkInfo -> splitApkInfo.apkFile.directory.containsFile("resources.arsc") } + .map { splitApkInfo -> splitApkInfo.apkFile } + .toList(), + ) + it.setIncludeAuxiliaryPublicXml(true) it.decodeResources(apkFilesPath) it.decodeManifest(apkFilesPath) } @@ -123,43 +149,199 @@ class ResourcePatchContext internal constructor( UsesFramework().apply { ids = resourcesDecoder.resTable.listFramePackages().map { it.id } } + + for ((splitName, splitApkInfo) in splitApkInfos) { + val splitFilesPath = splitApkFilesPath.kmpResolve(splitName).also { it.mkdirs() } + + // ABI splits contain only native libraries and no resource table. + // Decode only the manifest so its package attribute can be synced with the base. + if (!splitApkInfo.apkFile.directory.containsFile("resources.arsc")) { + ResourcesDecoder(resourceConfig, splitApkInfo).decodeManifest(splitFilesPath) + splitApkInfo.usesFramework = apkInfo.usesFramework + continue + } + + logger.info("Decoding resources for split \"$splitName\"") + + splitDecodingModes[splitName] = ALL + + val splitResourcesDecoder = + ResourcesDecoder(resourceConfig, splitApkInfo).also { + it.decodeResources(splitFilesPath) + it.decodeManifest(splitFilesPath) + } + + ApkDecoder(splitApkInfo, resourceConfig).recordUncompressedFiles(splitResourcesDecoder.resFileMapping) + + splitApkInfo.usesFramework = + UsesFramework().apply { + ids = splitResourcesDecoder.resTable.listFramePackages().map { it.id } + } + } + + /* + In split APKs, the base APK's resource table references drawables and values + that physically reside in density/config splits. Without merging these into + the base decode directory, AAPT fails to link resources during compilation. + + Symlinks are used to avoid duplicating files. + */ + mergeSplitResources() + } + + private fun mergeSplitResources() { + val baseResDir = apkFilesPath.kmpResolve("res") + if (!baseResDir.exists()) return + + for ((splitName, _) in splitApkInfos) { + val splitResDir = splitApkFilesPath.kmpResolve(splitName).kmpResolve("res") + if (!splitResDir.exists()) continue + + val resDirs = splitResDir.listFiles() ?: continue + for (resTypeDir in resDirs) { + if (!resTypeDir.isDirectory) continue + + val targetTypeDir = baseResDir.kmpResolve(resTypeDir.name) + targetTypeDir.mkdirs() + + val files = resTypeDir.listFiles() ?: continue + for (file in files) { + val targetFile = targetTypeDir.kmpResolve(file.name) + if (!targetFile.exists()) { + runCatching { + Files.createSymbolicLink(targetFile.toPath(), file.toPath().toAbsolutePath()) + }.getOrElse { + file.copyTo(targetFile) + } + } + } + } + } + } + + /** + * Ensure each split manifest's package attribute matches the base manifest. + */ + private fun syncSplitPackageNames() { + if (splitApkInfos.isEmpty()) return + + val baseManifest = apkFilesPath.kmpResolve("AndroidManifest.xml") + if (!baseManifest.exists()) return + + val basePackageName = Document(baseManifest).use { document -> + document.getElementsByTagName("manifest").item(0) + ?.attributes?.getNamedItem("package")?.textContent + } ?: return + + for ((splitName, _) in splitApkInfos) { + val splitManifest = splitApkFilesPath.kmpResolve(splitName).kmpResolve("AndroidManifest.xml") + if (!splitManifest.exists()) continue + + Document(splitManifest).use { document -> + document.getElementsByTagName("manifest").item(0) + ?.attributes?.getNamedItem("package")?.let { it.textContent = basePackageName } + } + } } /** - * Compile resources in [apkFilesPath]. + * Compile resources. * - * @return The [PatchesResult.PatchedResources]. + * @return The base [PatchesResult.PatchedResources] and per-split resources. */ - override fun get(): PatchesResult.PatchedResources { + override fun get() = getCompiledResources().first + + internal fun getCompiledResources(): Pair> { logger.info("Compiling patched resources") - val resourcesPath = patchedFilesPath.kmpResolve("resources").also { it.mkdirs() } + // Android requires all splits to have the same package as the base. + syncSplitPackageNames() - val resourcesApkFile = - if (decodingMode == ResourceDecodingMode.ALL) { - val resourcesApkFile = resourcesPath.kmpResolve("resources.apk").also { it.createNewFile() } + val baseResources = compileResources( + apkInfo = apkInfo, + decodedResourcesPath = apkFilesPath, + outputPath = patchedFilesPath.kmpResolve("resources").also { it.mkdirs() }, + decodingMode = decodingMode, + deleteResources = deleteResources, + ) - val manifestFile = - apkFilesPath.kmpResolve("AndroidManifest.xml").also(ResXmlUtils::fixingPublicAttrsInProviderAttributes) + val splitResources = compileSplitResources() - val resPath = apkFilesPath.kmpResolve("res") - val frameworkApkFiles = - with(Framework(resourceConfig)) { - apkInfo.usesFramework.ids.map { id -> getFrameworkApk(id, null) } - }.toTypedArray() + return baseResources to splitResources + } + + private fun compileSplitResources(): Map { + if (splitApkInfos.isEmpty()) return emptyMap() + + return buildMap { + for ((splitName, splitApkInfo) in splitApkInfos) { + val decodedResourcesPath = splitApkFilesPath.kmpResolve(splitName) + + // Splits without resources.arsc were not decoded and have no output directory. + if (!decodedResourcesPath.kmpResolve("res").exists() && + !decodedResourcesPath.kmpResolve("AndroidManifest.xml").exists() + ) continue + + logger.info("Compiling patched resources for split \"$splitName\"") - AaptInvoker( - resourceConfig, - apkInfo, - ).invoke(resourcesApkFile, manifestFile, resPath, null, null, frameworkApkFiles) + put(splitName, compileResources( + apkInfo = splitApkInfo, + decodedResourcesPath = decodedResourcesPath, + outputPath = patchedFilesPath.kmpResolve("splits").kmpResolve(splitName).also { it.mkdirs() }, + decodingMode = splitDecodingModes[splitName] ?: ResourceDecodingMode.MANIFEST, + deleteResources = splitDeleteResources[splitName] ?: emptySet(), + )) + } + } + } - resourcesApkFile + /** + * Compile decoded resources from [decodedResourcesPath] into [outputPath]. + * + * @param apkInfo The [ApkInfo] for the APK being compiled. + * @param decodedResourcesPath The path containing decoded resources. + * @param outputPath The path to write compiled output to. + * @param decodingMode How resources were decoded, determining compilation behavior. + * @param deleteResources Resources to delete from the APK. + */ + private fun compileResources( + apkInfo: ApkInfo, + decodedResourcesPath: File, + outputPath: File, + decodingMode: ResourceDecodingMode, + deleteResources: Set, + ): PatchesResult.PatchedResources { + val resourcesApkFile = + if (decodingMode != ResourceDecodingMode.NONE) { + val manifestFile = decodedResourcesPath.kmpResolve("AndroidManifest.xml") + if (!manifestFile.exists()) { + null + } else { + val resourcesApkFile = outputPath.kmpResolve("resources.apk").also { it.createNewFile() } + + manifestFile.also(ResXmlUtils::fixingPublicAttrsInProviderAttributes) + + // Pass null for resPath when only the manifest was decoded, + // which makes AAPT compile only the manifest to binary XML. + val resPath = decodedResourcesPath.kmpResolve("res").takeIf { decodingMode == ALL } + val frameworkApkFiles = + with(Framework(resourceConfig)) { + apkInfo.usesFramework?.ids?.map { id -> getFrameworkApk(id, null) } ?: emptyList() + }.toTypedArray() + + AaptInvoker( + resourceConfig, + apkInfo, + ).invoke(resourcesApkFile, manifestFile, resPath, null, null, frameworkApkFiles) + + resourcesApkFile + } } else { null } val otherFiles = - apkFilesPath.listFiles()!!.filter { + decodedResourcesPath.listFiles()?.filter { // Excluded because present in resources.other. // TODO: We are reusing apkFiles as a temporarily directory for extracting resources. // This is not ideal as it could conflict with files such as the ones that are filtered here. @@ -173,11 +355,11 @@ class ResourcePatchContext internal constructor( it.name != "res" && // Generated by Androlib. it.name != "build" - } + } ?: emptyList() + val otherResourceFiles = if (otherFiles.isNotEmpty()) { - // Move the other resources files. - resourcesPath.kmpResolve("other").also { it.mkdirs() }.apply { + outputPath.kmpResolve("other").also { it.mkdirs() }.apply { otherFiles.forEach { file -> Files.move(file.toPath(), kmpResolve(file.name).toPath()) } @@ -195,17 +377,17 @@ class ResourcePatchContext internal constructor( } /** - * Get a file from [apkFilesPath]. + * Get a file from the base APK's [apkFilesPath]. * * @param path The path of the file. - * @param copy Whether to copy the file from [apkFile] if it does not exist yet in [apkFilesPath]. + * @param copy Whether to copy the file from the base APK if it does not exist yet in [apkFilesPath]. */ operator fun get( path: String, copy: Boolean = true, ) = apkFilesPath.kmpResolve(path).apply { if (copy && !exists()) { - with(ExtFile(apkFile).directory) { + with(ExtFile(apk.file).directory) { if (containsFile(path) || containsDir(path)) { copyToDir(apkFilesPath, path) } @@ -214,12 +396,62 @@ class ResourcePatchContext internal constructor( } /** - * Mark a file for deletion when the APK is rebuilt. + * Get a file from a split APK. + * + * @param splitName The name of the split. + * @param path The path of the file within the split. + * @param copy Whether to copy the file from the split APK if it does not exist yet. + */ + fun split( + splitName: String, + path: String, + copy: Boolean = true, + ): File { + val splitApkInfo = splitApkInfos[splitName] + ?: throw PatchException("Split \"$splitName\" not found") + + val splitFilesPath = splitApkFilesPath.kmpResolve(splitName) + + return splitFilesPath.kmpResolve(path).apply { + if (copy && !exists()) { + with(splitApkInfo.apkFile.directory) { + if (containsFile(path) || containsDir(path)) { + copyToDir(splitFilesPath, path) + } + } + } + } + } + + /** + * Read and write documents in a split APK. + * + * @param splitName The name of the split. + * @param path The path of the document within the split. + */ + fun splitDocument(splitName: String, path: String) = Document(split(splitName, path)) + + /** + * Mark a file for deletion from the base APK when it is rebuilt. * * @param name The name of the file to delete. */ fun delete(name: String) = deleteResources.add(name) + /** + * Mark a file for deletion from a split APK when it is rebuilt. + * + * @param splitName The name of the split. + * @param name The name of the file to delete. + */ + fun deleteSplit(splitName: String, name: String) { + if (splitName !in splitApkInfos) { + throw PatchException("Split \"$splitName\" not found") + } + + splitDeleteResources.getOrPut(splitName) { mutableSetOf() }.add(name) + } + /** * How to handle resources decoding and compiling. */ diff --git a/patcher/src/jvmTest/kotlin/app/revanced/patcher/PatcherTestBase.kt b/patcher/src/jvmTest/kotlin/app/revanced/patcher/PatcherTestBase.kt index 21fe2e7f..b755e07d 100644 --- a/patcher/src/jvmTest/kotlin/app/revanced/patcher/PatcherTestBase.kt +++ b/patcher/src/jvmTest/kotlin/app/revanced/patcher/PatcherTestBase.kt @@ -79,7 +79,7 @@ abstract class PatcherTestBase { every { opcodes } returns Opcodes.getDefault() } - every { this@bytecodePatchContext.getProperty("apkFile") } returns mockk() + every { this@bytecodePatchContext.getProperty("apk") } returns Apk.Single(mockk()) every { this@bytecodePatchContext.classDefs } returns ClassDefs().apply {