finish first working version

This commit is contained in:
oSumAtrIX 2024-12-01 09:15:02 +01:00
parent a5a14ac31a
commit 5c1905f937
No known key found for this signature in database
GPG Key ID: A9B3094ACDB604B4
26 changed files with 130 additions and 112 deletions

View File

@ -1,137 +1,155 @@
package app.revanced.patches.all.layout.branding package app.revanced.patches.all.layout.branding
import app.revanced.patcher.patch.PatchException import app.revanced.patcher.patch.*
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 app.revanced.util.getNode
import java.io.File import app.revanced.util.inputStreamFromBundledResource
import java.io.FilenameFilter 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( val changeIconPatch = resourcePatch(
name = "Change icon", name = "Change icon",
description = "Changes the app icon to a custom icon. By default, the \"ReVanced icon\" is used.", description = "Changes the app icon to a custom icon. By default, the ReVanced icon is used.",
use = false, use = false,
) { ) {
val revancedIconOptionValue = "" // Empty value == ReVanced icon. val revancedIconOptionValue = emptyList<String>() // Empty list == ReVanced icon.
val pixelDensities = setOf( val pixelDensities = setOf(
"xxxhdpi",
"xxhdpi",
"xhdpi",
"hdpi",
"mdpi", "mdpi",
"hdpi",
"xhdpi",
"xxhdpi",
"xxxhdpi",
) )
val iconOptions = buildMap { val iconOptions = pixelDensities.associateWith { pixelDensity ->
arrayOf("foreground", "background", "monochrome").forEach { iconType -> stringsOption(
this += pixelDensities.associateBy { key = "${pixelDensity}Icons",
stringOption(
key = "${iconType}IconPath",
default = revancedIconOptionValue, default = revancedIconOptionValue,
values = mapOf("ReVanced Logo" to revancedIconOptionValue), values = mapOf("ReVanced logo" to revancedIconOptionValue),
title = "Icon file path (Pixel density: $it, Icon type: $iconType)", title = "Icons (Pixel density: $pixelDensity)",
description = "The path to the icon file to apply to the app for the pixel density $it " + description = buildString {
"and icon type $iconType.", 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)")
// This might confuse the user. appendLine("4. Foreground icon (optional)")
put( appendLine("5. Monochrome icon (optional, Android 13+")
"full", appendLine("\nIcons must be provided in the same order as listed above. Missing optional icons can be skipped by leaving the field empty.")
stringOption( appendLine("\nYou can create custom icon sets at https://icon.kitchen.")
key = "fullIconPath", },
default = revancedIconOptionValue, required = true,
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 { execute {
manifest { val firstPixelDensity = pixelDensities.first()
val applicationNode = getNode("application")
val iconResourceReference = applicationNode.attributes.getNamedItem("android:icon").textContent!!
val iconResourceFiles = resolve(iconResourceReference) fun patchIcon(
getIcon: (String, Int) -> String?,
readIcon: (String) -> InputStream,
) {
// Any density, as the user should provide the icons for all densities.
iconResourceFiles.forEach { resourceFile -> // region Change the app icon in the AndroidManifest.xml file.
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. // 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 { } else {
// TODO: Replace the icon with fullIcon. ""
}
}
}
}
}
context(ResourcePatchContext)
fun <T> manifest(block: Document.() -> T) = document("AndroidManifest.xml").use(block)
context(ResourcePatchContext)
private fun resolve(resourceReference: String): List<File> {
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 // 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(
"""
private class IconResource( <?xml version="1.0" encoding="utf-8"?>
val file: File, <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
val pixelDensity: String, <background android:drawable="@mipmap/ic_launcher_background"/>
) <foreground android:drawable="@mipmap/ic_launcher_foreground"/>
$monochromeIconXmlString
context(ResourcePatchContext) </adaptive-icon>
private fun parseAdaptiveIcon(xmlFile: File) = document(xmlFile.absolutePath).use { adaptiveIconNode -> """.trimIndent(),
val adaptiveIcon = adaptiveIconNode.getNode("adaptive-icon")
fun getIconResourceReference(iconType: String): List<IconResource>? {
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( pixelDensities.forEach { pixelDensity ->
val background: List<IconResource>, val icon = getIcon(pixelDensity, FULL_ICON)!!
val foreground: List<IconResource>, // Safe call (?.) is used because the user may just provide the full icon and skip the other optional icons.
val monochrome: List<IconResource>?, 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()
}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB