From 6555f6e6f8b52c2f1ddab1f52c6704cd2d8cfc12 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Sat, 11 Oct 2025 12:00:26 +0400 Subject: [PATCH] fix(YouTube - Custom branding): Do not add a broken custom icon if the user provides an invalid custom icon path --- .../layout/branding/CustomBrandingPatch.kt | 3 +- .../branding/BaseCustomBrandingPatch.kt | 250 +++++++++--------- 2 files changed, 132 insertions(+), 121 deletions(-) diff --git a/patches/src/main/kotlin/app/revanced/patches/music/layout/branding/CustomBrandingPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/layout/branding/CustomBrandingPatch.kt index 6f0f46026..b1c811de2 100644 --- a/patches/src/main/kotlin/app/revanced/patches/music/layout/branding/CustomBrandingPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/layout/branding/CustomBrandingPatch.kt @@ -4,6 +4,7 @@ import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWith import app.revanced.patcher.extensions.InstructionExtensions.getInstruction import app.revanced.patcher.patch.bytecodePatch import app.revanced.patcher.util.smali.ExternalLabel +import app.revanced.patches.music.misc.extension.sharedExtensionPatch import app.revanced.patches.music.misc.gms.Constants.MUSIC_MAIN_ACTIVITY_NAME import app.revanced.patches.music.misc.gms.Constants.MUSIC_PACKAGE_NAME import app.revanced.patches.music.misc.gms.musicActivityOnCreateFingerprint @@ -68,7 +69,7 @@ val customBrandingPatch = baseCustomBrandingPatch( preferenceScreen = PreferenceScreen.GENERAL, block = { - dependsOn(disableSplashAnimationPatch) + dependsOn(sharedExtensionPatch, disableSplashAnimationPatch) compatibleWith( "com.google.android.apps.youtube.music"( diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/layout/branding/BaseCustomBrandingPatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/layout/branding/BaseCustomBrandingPatch.kt index d8356aee0..7b46b820f 100644 --- a/patches/src/main/kotlin/app/revanced/patches/shared/layout/branding/BaseCustomBrandingPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/shared/layout/branding/BaseCustomBrandingPatch.kt @@ -21,7 +21,6 @@ import app.revanced.util.findElementByAttributeValueOrThrow import app.revanced.util.removeFromParent import app.revanced.util.returnEarly import org.w3c.dom.Element -import org.w3c.dom.Node import org.w3c.dom.NodeList import java.io.File import java.util.logging.Logger @@ -106,6 +105,7 @@ internal fun baseCustomBrandingPatch( dependsOn( addResourcesPatch, + bytecodePatch { execute { mainActivityOnCreateFingerprint.method.addInstruction( @@ -201,6 +201,135 @@ internal fun baseCustomBrandingPatch( ) } + document("AndroidManifest.xml").use { document -> + // Create launch aliases that can be programmatically selected in app. + fun createAlias( + aliasName: String, + iconMipmapName: String, + appNameIndex: Int, + useCustomName: Boolean, + enabled: Boolean, + intents: NodeList + ): Element { + val label = if (useCustomName) { + if (customName == null) { + "Custom" // Dummy text, and normally cannot be seen. + } else { + customName!! + } + } else if (appNameIndex == 1) { + // Indexing starts at 1. + originalAppName + } else { + "@string/revanced_custom_branding_name_entry_$appNameIndex" + } + val alias = document.createElement("activity-alias") + alias.setAttribute("android:name", aliasName) + alias.setAttribute("android:enabled", enabled.toString()) + alias.setAttribute("android:exported", "true") + alias.setAttribute("android:icon", "@mipmap/$iconMipmapName") + alias.setAttribute("android:label", label) + alias.setAttribute("android:targetActivity", mainActivityName) + + // Copy all intents from the original alias so long press actions still work. + if (copyExistingIntentsToAliases) { + for (i in 0 until intents.length) { + alias.appendChild( + intents.item(i).cloneNode(true) + ) + } + } else { + val intentFilter = document.createElement("intent-filter").apply { + val action = document.createElement("action") + action.setAttribute("android:name", "android.intent.action.MAIN") + appendChild(action) + + val category = document.createElement("category") + category.setAttribute("android:name", "android.intent.category.LAUNCHER") + appendChild(category) + } + alias.appendChild(intentFilter) + } + + return alias + } + + val application = document.getElementsByTagName("application").item(0) as Element + val intentFilters = document.childNodes.findElementByAttributeValueOrThrow( + "android:name", + activityAliasNameWithIntents + ).childNodes + + // The YT application name can appear in some places along side the system + // YouTube app, such as the settings app list and in the "open with" file picker. + // Because the YouTube app cannot be completely uninstalled and only disabled, + // use a custom name for this situation to disambiguate which app is which. + application.setAttribute( + "android:label", + "@string/revanced_custom_branding_name_entry_2" + ) + + for (appNameIndex in 1 .. numberOfPresetAppNames) { + fun aliasName(name: String): String = ".revanced_" + name + '_' + appNameIndex + + val useCustomNameLabel = (useCustomName && appNameIndex == numberOfPresetAppNames) + + // Original icon. + application.appendChild( + createAlias( + aliasName = aliasName(ORIGINAL_USER_ICON_STYLE_NAME), + iconMipmapName = originalLauncherIconName, + appNameIndex = appNameIndex, + useCustomName = useCustomNameLabel, + enabled = (appNameIndex == 1), + intentFilters + ) + ) + + // Bundled icons. + iconStyleNames.forEachIndexed { index, style -> + application.appendChild( + createAlias( + aliasName = aliasName(style), + iconMipmapName = LAUNCHER_RESOURCE_NAME_PREFIX + style, + appNameIndex = appNameIndex, + useCustomName = useCustomNameLabel, + enabled = false, + intentFilters + ) + ) + } + + // User provided custom icon. + // + // Must add all aliases even if the user did not provide a custom icon of their own. + // This is because if the user installs with an option, then repatches without the option, + // the alias must still exist because if it was previously enabled and then it's removed + // the app will become broken and cannot launch. Even if the app data is cleared + // it still cannot be launched and the only fix is to uninstall the app. + // To prevent this, always include all aliases and use dummy data if needed. + application.appendChild( + createAlias( + aliasName = aliasName(CUSTOM_USER_ICON_STYLE_NAME), + iconMipmapName = LAUNCHER_RESOURCE_NAME_PREFIX + CUSTOM_USER_ICON_STYLE_NAME, + appNameIndex = appNameIndex, + useCustomName = useCustomNameLabel, + enabled = false, + intentFilters + ) + ) + } + + // Remove the main action from the original alias, otherwise two apps icons + // can be shown in the launcher. Can only be done after adding the new aliases. + intentFilters.findElementByAttributeValueOrThrow( + "android:name", + "android.intent.action.MAIN" + ).removeFromParent() + } + + // Copy custom icons last, so if the user enters an invalid icon path + // and an exception is thrown then the critical manifest changes are still made. if (useCustomIcon) { // Copy user provided files val iconPathFile = File(customIcon!!.trim()) @@ -263,125 +392,6 @@ internal fun baseCustomBrandingPatch( } } - document("AndroidManifest.xml").use { document -> - // Create launch aliases that can be programmatically selected in app. - fun createAlias( - aliasName: String, - iconMipmapName: String, - appNameIndex: Int, - useCustomName: Boolean, - enabled: Boolean, - intents: NodeList - ): Element { - val label = if (useCustomName) { - if (customName == null) { - "Custom" // Dummy text, and normally cannot be seen. - } else { - customName!! - } - } else if (appNameIndex == 1) { - // Indexing starts at 1. - originalAppName - } else { - "@string/revanced_custom_branding_name_entry_$appNameIndex" - } - val alias = document.createElement("activity-alias") - alias.setAttribute("android:name", aliasName) - alias.setAttribute("android:enabled", enabled.toString()) - alias.setAttribute("android:exported", "true") - alias.setAttribute("android:icon", "@mipmap/$iconMipmapName") - alias.setAttribute("android:label", label) - alias.setAttribute("android:targetActivity", mainActivityName) - - // Copy all intents from the original alias so long press actions still work. - if (copyExistingIntentsToAliases) { - for (i in 0 until intents.length) { - alias.appendChild( - intents.item(i).cloneNode(true) - ) - } - } else { - val intentFilter = document.createElement("intent-filter").apply { - val action = document.createElement("action") - action.setAttribute("android:name", "android.intent.action.MAIN") - appendChild(action) - - val category = document.createElement("category") - category.setAttribute("android:name", "android.intent.category.LAUNCHER") - appendChild(category) - } - alias.appendChild(intentFilter) - } - - return alias - } - - val intentFilters = document.childNodes.findElementByAttributeValueOrThrow( - "android:name", - activityAliasNameWithIntents - ).childNodes - - val application = document.getElementsByTagName("application").item(0) as Element - - for (appNameIndex in 1 .. numberOfPresetAppNames) { - fun aliasName(name: String): String = ".revanced_" + name + '_' + appNameIndex - - val useCustomNameLabel = (useCustomName && appNameIndex == numberOfPresetAppNames) - - // Original icon. - application.appendChild( - createAlias( - aliasName = aliasName(ORIGINAL_USER_ICON_STYLE_NAME), - iconMipmapName = originalLauncherIconName, - appNameIndex = appNameIndex, - useCustomName = useCustomNameLabel, - enabled = (appNameIndex == 1), - intentFilters - ) - ) - - // Bundled icons. - iconStyleNames.forEachIndexed { index, style -> - application.appendChild( - createAlias( - aliasName = aliasName(style), - iconMipmapName = LAUNCHER_RESOURCE_NAME_PREFIX + style, - appNameIndex = appNameIndex, - useCustomName = useCustomNameLabel, - enabled = false, - intentFilters - ) - ) - } - - // User provided custom icon. - // - // Must add all aliases even if the user did not provide a custom icon of their own. - // This is because if the user installs with an option, then repatches without the option, - // the alias must still exist because if it was previously enabled and then it's removed - // the app will become broken and cannot launch. Even if the app data is cleared - // it still cannot be launched and the only fix is to uninstall the app. - // To prevent this, always include all aliases and use dummy data if needed. - application.appendChild( - createAlias( - aliasName = aliasName(CUSTOM_USER_ICON_STYLE_NAME), - iconMipmapName = LAUNCHER_RESOURCE_NAME_PREFIX + CUSTOM_USER_ICON_STYLE_NAME, - appNameIndex = appNameIndex, - useCustomName = useCustomNameLabel, - enabled = false, - intentFilters - ) - ) - } - - // Remove the main action from the original alias, otherwise two apps icons - // can be shown in the launcher. Can only be done after adding the new aliases. - intentFilters.findElementByAttributeValueOrThrow( - "android:name", - "android.intent.action.MAIN" - ).removeFromParent() - } - executeBlock() } }