diff --git a/patches/api/patches.api b/patches/api/patches.api index 2ba4d9a4e..dc5e67d40 100644 --- a/patches/api/patches.api +++ b/patches/api/patches.api @@ -1,3 +1,7 @@ +public final class app/revanced/patches/all/layout/branding/IconPatchKt { + public static final fun getChangeIconPatch ()Lapp/revanced/patcher/patch/ResourcePatch; +} + public final class app/revanced/patches/all/misc/activity/exportall/ExportAllActivitiesPatchKt { public static final fun getExportAllActivitiesPatch ()Lapp/revanced/patcher/patch/ResourcePatch; } diff --git a/patches/src/main/kotlin/app/revanced/patches/all/layout/branding/IconPatch.kt b/patches/src/main/kotlin/app/revanced/patches/all/layout/branding/IconPatch.kt new file mode 100644 index 000000000..fbd536649 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/all/layout/branding/IconPatch.kt @@ -0,0 +1,137 @@ +package app.revanced.patches.all.layout.branding + +import app.revanced.patcher.patch.PatchException +import app.revanced.patcher.patch.ResourcePatchContext +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patcher.patch.stringOption +import app.revanced.patcher.util.Document +import app.revanced.util.getNode +import java.io.File +import java.io.FilenameFilter + +val changeIconPatch = resourcePatch( + name = "Change icon", + description = "Changes the app icon to a custom icon. By default, the \"ReVanced icon\" is used.", + use = false, +) { + val revancedIconOptionValue = "" // Empty value == ReVanced icon. + + val pixelDensities = setOf( + "xxxhdpi", + "xxhdpi", + "xhdpi", + "hdpi", + "mdpi", + ) + + val iconOptions = buildMap { + arrayOf("foreground", "background", "monochrome").forEach { iconType -> + this += pixelDensities.associateBy { + stringOption( + key = "${iconType}IconPath", + default = revancedIconOptionValue, + values = mapOf("ReVanced Logo" to revancedIconOptionValue), + title = "Icon file path (Pixel density: $it, Icon type: $iconType)", + description = "The path to the icon file to apply to the app for the pixel density $it " + + "and icon type $iconType.", + ) + } + } + + // This might confuse the user. + put( + "full", + stringOption( + key = "fullIconPath", + default = revancedIconOptionValue, + values = mapOf("ReVanced Logo" to revancedIconOptionValue), + title = "Full icon file path", + description = "The path to the icon file to apply when the app " + + "does not have a specific icon for the pixel density.", + ), + ) + } + + execute { + manifest { + val applicationNode = getNode("application") + val iconResourceReference = applicationNode.attributes.getNamedItem("android:icon").textContent!! + + val iconResourceFiles = resolve(iconResourceReference) + + iconResourceFiles.forEach { resourceFile -> + if (resourceFile.extension == "xml" && resourceFile.name.startsWith("ic_launcher")) { + val adaptiveIcon = parseAdaptiveIcon(resourceFile) + + // TODO: Replace the background, foreground, and monochrome icons with the custom icons. + } else { + // TODO: Replace the icon with fullIcon. + } + } + } + } +} + +context(ResourcePatchContext) +fun manifest(block: Document.() -> T) = document("AndroidManifest.xml").use(block) + +context(ResourcePatchContext) +private fun resolve(resourceReference: String): List { + val isMipmap = resourceReference.startsWith("@mipmap/") + val isDrawable = resourceReference.startsWith("@drawable/") + + val directories = get("res").listFiles( + if (isMipmap) { + FilenameFilter { _, name -> name.startsWith("mipmap-") } + } else if (isDrawable) { + FilenameFilter { _, name -> name.startsWith("drawable-") } + } else { + throw PatchException("Unsupported resource reference: $resourceReference") + }, + )!! + + // The name does not have an extension. It is the name of the resource. + val resourceName = resourceReference.split("/").last() + val resources = directories.mapNotNull { + // Find the first file that starts with the resource name. + it.listFiles { _, name -> name.startsWith(resourceName) }!!.firstOrNull() + } + + return resources +} + +private class IconResource( + val file: File, + val pixelDensity: String, +) + +context(ResourcePatchContext) +private fun parseAdaptiveIcon(xmlFile: File) = document(xmlFile.absolutePath).use { adaptiveIconNode -> + val adaptiveIcon = adaptiveIconNode.getNode("adaptive-icon") + + fun getIconResourceReference(iconType: String): List? { + val resourceReferenceString = adaptiveIcon.getNode(iconType)?.let { + it.attributes.getNamedItem("android:drawable").textContent!! + } + + if (resourceReferenceString == null) { + return null + } + + return resolve(resourceReferenceString).map { + IconResource(file = it, pixelDensity = it.parentFile.name.split("-").last()) + } + } + + AdaptiveIcon( + getIconResourceReference("background")!!, + getIconResourceReference("foreground")!!, + getIconResourceReference("monochrome"), + ) +} + +private class AdaptiveIcon( + val background: List, + val foreground: List, + val monochrome: List?, +) diff --git a/patches/src/main/kotlin/app/revanced/patches/music/layout/compactheader/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/music/layout/compactheader/Fingerprints.kt index d7f0f03de..ea6c1603d 100644 --- a/patches/src/main/kotlin/app/revanced/patches/music/layout/compactheader/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/layout/compactheader/Fingerprints.kt @@ -1,8 +1,8 @@ package app.revanced.patches.music.layout.compactheader -import com.android.tools.smali.dexlib2.Opcode -import com.android.tools.smali.dexlib2.AccessFlags import app.revanced.patcher.fingerprint +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode internal val constructCategoryBarFingerprint = fingerprint { accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR) @@ -16,7 +16,5 @@ internal val constructCategoryBarFingerprint = fingerprint { Opcode.IPUT_OBJECT, Opcode.CONST, Opcode.INVOKE_VIRTUAL, - Opcode.NEW_INSTANCE, - Opcode.INVOKE_DIRECT, ) } diff --git a/patches/src/main/kotlin/app/revanced/util/ResourceUtils.kt b/patches/src/main/kotlin/app/revanced/util/ResourceUtils.kt index 7a1496afc..fad29eaab 100644 --- a/patches/src/main/kotlin/app/revanced/util/ResourceUtils.kt +++ b/patches/src/main/kotlin/app/revanced/util/ResourceUtils.kt @@ -23,16 +23,14 @@ fun NodeList.asSequence() = (0 until this.length).asSequence().map { this.item(i * Returns a sequence for all child nodes. */ @Suppress("UNCHECKED_CAST") -fun Node.childElementsSequence() = - this.childNodes.asSequence().filter { it.nodeType == Node.ELEMENT_NODE } as Sequence +fun Node.childElementsSequence() = this.childNodes.asSequence().filter { it.nodeType == Node.ELEMENT_NODE } as Sequence /** * Performs the given [action] on each child element. */ -inline fun Node.forEachChildElement(action: (Element) -> Unit) = - childElementsSequence().forEach { - action(it) - } +inline fun Node.forEachChildElement(action: (Element) -> Unit) = childElementsSequence().forEach { + action(it) +} /** * Recursively traverse the DOM tree starting from the given root node. @@ -141,7 +139,8 @@ internal fun Node.addResource( appendChild(resource.serialize(ownerDocument, resourceCallback)) } -internal fun org.w3c.dom.Document.getNode(tagName: String) = this.getElementsByTagName(tagName).item(0) +internal fun org.w3c.dom.Document.getNode(tagName: String) = getElementsByTagName(tagName).item(0) +internal fun Node.getNode(tagName: String) = childNodes.asSequence().find { it.nodeName == tagName } internal fun NodeList.findElementByAttributeValue(attributeName: String, value: String): Element? { for (i in 0 until length) { @@ -164,8 +163,7 @@ internal fun NodeList.findElementByAttributeValue(attributeName: String, value: return null } -internal fun NodeList.findElementByAttributeValueOrThrow(attributeName: String, value: String) = - findElementByAttributeValue(attributeName, value) ?: throw PatchException("Could not find: $attributeName $value") +internal fun NodeList.findElementByAttributeValueOrThrow(attributeName: String, value: String) = findElementByAttributeValue(attributeName, value) ?: throw PatchException("Could not find: $attributeName $value") internal fun Element.copyAttributesFrom(oldContainer: Element) { // Copy attributes from the old element to the new element