Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add Change icon patch #4020

Draft
wants to merge 3 commits into
base: dev
Choose a base branch
from
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
4 changes: 4 additions & 0 deletions patches/api/patches.api
Original file line number Diff line number Diff line change
@@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package app.revanced.patches.all.layout.branding

import app.revanced.patcher.patch.*
import app.revanced.util.getNode
import app.revanced.util.inputStreamFromBundledResource
import java.io.InputStream
import java.nio.file.Files
import java.nio.file.StandardCopyOption

private const val FULL_ICON = 0
private const val ROUND_ICON = 1
private const val BACKGROUND_ICON = 2
private const val FOREGROUND_ICON = 3
private const val MONOCHROME_ICON = 4

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 = emptyList<String>() // Empty list == ReVanced icon.

val pixelDensities = setOf(
"mdpi",
"hdpi",
"xhdpi",
"xxhdpi",
"xxxhdpi",
)

val iconOptions = pixelDensities.associateWith { pixelDensity ->
stringsOption(
key = "${pixelDensity}Icons",
default = revancedIconOptionValue,
values = mapOf("ReVanced logo" to revancedIconOptionValue),
title = "Icons (Pixel density: $pixelDensity)",
description = buildString {
appendLine("Provide paths to the following icons for pixel density $pixelDensity (PNG, JPG, WEBP, or vector drawable XML):")
appendLine("1. Launcher icon (required)")
appendLine("2. Round icon (optional, Android 7+)")
appendLine("\nYou can use adaptive icons (Android 8+) by providing the following additional icons:")
appendLine("\n3. Background icon (optional)")
appendLine("4. Foreground icon (optional)")
appendLine("5. Monochrome icon (optional, Android 13+")
appendLine("\nIcons must be provided in the same order as listed above. Missing optional icons can be skipped by leaving the field empty.")
appendLine("\nYou can create custom icon sets at https://icon.kitchen.")
},
required = true,
)
}

execute {
val firstPixelDensity = pixelDensities.first()

fun patchIcon(
getIcon: (String, Int) -> String?,
readIcon: (String) -> InputStream,
) {
// Any density, as the user should provide the icons for all densities.

// region Change the app icon in the AndroidManifest.xml file.

// If a round icon is provided, set the android:roundIcon attribute.
document("AndroidManifest.xml").use {
it.getNode("application").attributes.apply {
getNamedItem("android:icon").textContent = "@mipmap/ic_launcher"

val roundIcon = getIcon(firstPixelDensity, ROUND_ICON)
if (roundIcon?.isNotEmpty() == true) {
val roundIconAttribute = getNamedItem("android:roundIcon")
?: setNamedItem(it.createAttribute("android:roundIcon"))
roundIconAttribute.textContent = "@mipmap/ic_launcher_round"
}
}
}

// endregion

// region Change the app icon for each pixel density.

val hasAdaptiveIcon = getIcon(firstPixelDensity, BACKGROUND_ICON)

if (hasAdaptiveIcon?.isNotEmpty() == true) {
val monochromeIconXmlString = if (getIcon(firstPixelDensity, MONOCHROME_ICON)?.isNotEmpty() == true) {
"<monochrome android:drawable=\"@drawable/ic_launcher_monochrome\"/>"
} else {
""
}

// If an adaptive icon is provided, add the adaptive icon XML file to the res/mipmap-anydpi directory.
get("res/mipmap-anydpi/ic_launcher.xml").writeText(
"""
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@mipmap/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
$monochromeIconXmlString
</adaptive-icon>
""".trimIndent(),
)
}

pixelDensities.forEach { pixelDensity ->
val icon = getIcon(pixelDensity, FULL_ICON)!!
// Safe call (?.) is used because the user may just provide the full icon and skip the other optional icons.
val roundIcon = getIcon(pixelDensity, ROUND_ICON)
val backgroundIcon = getIcon(pixelDensity, BACKGROUND_ICON)
val foregroundIcon = getIcon(pixelDensity, FOREGROUND_ICON)
val monochromeIcon = getIcon(pixelDensity, MONOCHROME_ICON)

infix fun String?.to(target: String) {
if (isNullOrEmpty()) {
return
}

Files.copy(
readIcon(this),
get("res/$target").toPath(),
StandardCopyOption.REPLACE_EXISTING,
)
}

// Copy the icons to the mipmap directory.
icon to "mipmap-$pixelDensity/ic_launcher.png"
roundIcon to "mipmap-$pixelDensity/ic_launcher_round.png"
backgroundIcon to "mipmap-$pixelDensity/ic_launcher_background.png"
foregroundIcon to "mipmap-$pixelDensity/ic_launcher_foreground.png"
monochromeIcon to "drawable-$pixelDensity/ic_launcher_monochrome.png"
}

// endregion
}

if (iconOptions[firstPixelDensity]!!.value === revancedIconOptionValue) {
patchIcon({ pixelDensity, iconIndex ->
when (iconIndex) {
FULL_ICON -> "mipmap-$pixelDensity/revanced-icon"
ROUND_ICON -> "mipmap-$pixelDensity/revanced-icon-round"
BACKGROUND_ICON -> "mipmap-$pixelDensity/revanced-icon-background"
FOREGROUND_ICON -> "mipmap-$pixelDensity/revanced-icon-foreground"
MONOCHROME_ICON -> "drawable-$pixelDensity/revanced-icon-monochrome"
else -> throw IllegalArgumentException("Invalid icon index: $iconIndex")
}
}) { icon ->
inputStreamFromBundledResource("change-icon", "$icon.png")!!
}
} else {
patchIcon({ pixelDensity, iconIndex ->
iconOptions[pixelDensity]?.value?.get(iconIndex)
}) { icon ->
get(icon).inputStream()
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -15,6 +15,6 @@ internal val constructCategoryBarFingerprint = fingerprint {
Opcode.MOVE_RESULT_OBJECT,
Opcode.IPUT_OBJECT,
Opcode.CONST,
Opcode.INVOKE_VIRTUAL
Opcode.INVOKE_VIRTUAL,
)
}
16 changes: 7 additions & 9 deletions patches/src/main/kotlin/app/revanced/util/ResourceUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<Element>
fun Node.childElementsSequence() = this.childNodes.asSequence().filter { it.nodeType == Node.ELEMENT_NODE } as Sequence<Element>

/**
* 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.
Expand Down Expand Up @@ -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) {
Expand All @@ -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
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading