From 0ee36939f43f325afca37119db1cf1af3b63be27 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Sun, 13 Apr 2025 16:26:59 -0300 Subject: [PATCH] feat(Spotify - Custom theme): Add option to use unmodified player background gradient (#4741) Co-authored-by: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> --- patches/api/patches.api | 12 + .../layout/theme/CustomThemeBytecodePatch.kt | 82 ----- .../spotify/layout/theme/CustomThemePatch.kt | 170 +++++++++- .../spotify/layout/theme/Fingerprints.kt | 6 +- .../patches/spotify/layout/theme/Options.kt | 36 --- .../spotify/misc/UnlockPremiumPatch.kt | 13 +- .../speed/custom/CustomPlaybackSpeedPatch.kt | 6 +- .../kotlin/app/revanced/util/BytecodeUtils.kt | 306 ++++++++++++------ 8 files changed, 398 insertions(+), 233 deletions(-) delete mode 100644 patches/src/main/kotlin/app/revanced/patches/spotify/layout/theme/CustomThemeBytecodePatch.kt delete mode 100644 patches/src/main/kotlin/app/revanced/patches/spotify/layout/theme/Options.kt diff --git a/patches/api/patches.api b/patches/api/patches.api index 4bca2c5bf..52dd8348a 100644 --- a/patches/api/patches.api +++ b/patches/api/patches.api @@ -1525,7 +1525,11 @@ public final class app/revanced/patches/yuka/misc/unlockpremium/UnlockPremiumPat } public final class app/revanced/util/BytecodeUtilsKt { + public static final fun addInstructionsAtControlFlowLabel (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;ILjava/lang/String;)V + public static final fun containsLiteralInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;D)Z + public static final fun containsLiteralInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;F)Z public static final fun containsLiteralInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;J)Z + public static final fun findFreeRegister (Lcom/android/tools/smali/dexlib2/iface/Method;I[I)I public static final fun findInstructionIndicesReversed (Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/Opcode;)Ljava/util/List; public static final fun findInstructionIndicesReversed (Lcom/android/tools/smali/dexlib2/iface/Method;Lkotlin/jvm/functions/Function1;)Ljava/util/List; public static final fun findInstructionIndicesReversedOrThrow (Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/Opcode;)Ljava/util/List; @@ -1552,9 +1556,17 @@ public final class app/revanced/util/BytecodeUtilsKt { public static final fun indexOfFirstInstructionReversedOrThrow (Lcom/android/tools/smali/dexlib2/iface/Method;Ljava/lang/Integer;Lkotlin/jvm/functions/Function1;)I public static synthetic fun indexOfFirstInstructionReversedOrThrow$default (Lcom/android/tools/smali/dexlib2/iface/Method;Ljava/lang/Integer;Lcom/android/tools/smali/dexlib2/Opcode;ILjava/lang/Object;)I public static synthetic fun indexOfFirstInstructionReversedOrThrow$default (Lcom/android/tools/smali/dexlib2/iface/Method;Ljava/lang/Integer;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)I + public static final fun indexOfFirstLiteralInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;D)I + public static final fun indexOfFirstLiteralInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;F)I public static final fun indexOfFirstLiteralInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;J)I + public static final fun indexOfFirstLiteralInstructionOrThrow (Lcom/android/tools/smali/dexlib2/iface/Method;D)I + public static final fun indexOfFirstLiteralInstructionOrThrow (Lcom/android/tools/smali/dexlib2/iface/Method;F)I public static final fun indexOfFirstLiteralInstructionOrThrow (Lcom/android/tools/smali/dexlib2/iface/Method;J)I + public static final fun indexOfFirstLiteralInstructionReversed (Lcom/android/tools/smali/dexlib2/iface/Method;D)I + public static final fun indexOfFirstLiteralInstructionReversed (Lcom/android/tools/smali/dexlib2/iface/Method;F)I public static final fun indexOfFirstLiteralInstructionReversed (Lcom/android/tools/smali/dexlib2/iface/Method;J)I + public static final fun indexOfFirstLiteralInstructionReversedOrThrow (Lcom/android/tools/smali/dexlib2/iface/Method;D)I + public static final fun indexOfFirstLiteralInstructionReversedOrThrow (Lcom/android/tools/smali/dexlib2/iface/Method;F)I public static final fun indexOfFirstLiteralInstructionReversedOrThrow (Lcom/android/tools/smali/dexlib2/iface/Method;J)I public static final fun indexOfFirstResourceId (Lcom/android/tools/smali/dexlib2/iface/Method;Ljava/lang/String;)I public static final fun indexOfFirstResourceIdOrThrow (Lcom/android/tools/smali/dexlib2/iface/Method;Ljava/lang/String;)I diff --git a/patches/src/main/kotlin/app/revanced/patches/spotify/layout/theme/CustomThemeBytecodePatch.kt b/patches/src/main/kotlin/app/revanced/patches/spotify/layout/theme/CustomThemeBytecodePatch.kt deleted file mode 100644 index 2f639ef8d..000000000 --- a/patches/src/main/kotlin/app/revanced/patches/spotify/layout/theme/CustomThemeBytecodePatch.kt +++ /dev/null @@ -1,82 +0,0 @@ -package app.revanced.patches.spotify.layout.theme - -import app.revanced.patcher.extensions.InstructionExtensions.addInstructions -import app.revanced.patcher.extensions.InstructionExtensions.getInstruction -import app.revanced.patcher.fingerprint -import app.revanced.patcher.patch.bytecodePatch -import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod -import app.revanced.patches.spotify.misc.extension.IS_SPOTIFY_LEGACY_APP_TARGET -import app.revanced.patches.spotify.misc.extension.sharedExtensionPatch -import app.revanced.util.* -import com.android.tools.smali.dexlib2.AccessFlags -import com.android.tools.smali.dexlib2.Opcode -import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction -import com.android.tools.smali.dexlib2.iface.reference.FieldReference - -private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/spotify/layout/theme/CustomThemePatch;" - -internal val customThemeByteCodePatch = bytecodePatch { - dependsOn(sharedExtensionPatch) - - val backgroundColor by spotifyBackgroundColor - val backgroundColorSecondary by spotifyBackgroundColorSecondary - - execute { - if (IS_SPOTIFY_LEGACY_APP_TARGET) { - // Bytecode changes are not needed for legacy app target. - // Player background color is changed with existing resource patch. - return@execute - } - - fun MutableMethod.addColorChangeInstructions(literal: Long, colorString: String) { - val index = indexOfFirstLiteralInstructionOrThrow(literal) - val register = getInstruction(index).registerA - - addInstructions( - index + 1, - """ - const-string v$register, "$colorString" - invoke-static { v$register }, $EXTENSION_CLASS_DESCRIPTOR->getThemeColor(Ljava/lang/String;)J - move-result-wide v$register - """ - ) - } - - val encoreColorsClassName = with(encoreThemeFingerprint) { - // Find index of the first static get found after the string constant. - val encoreColorsFieldReferenceIndex = originalMethod.indexOfFirstInstructionOrThrow( - stringMatches!!.first().index, - Opcode.SGET_OBJECT - ) - - originalMethod.getInstruction(encoreColorsFieldReferenceIndex) - .getReference()!!.definingClass - } - - val encoreColorsConstructorFingerprint = fingerprint { - accessFlags(AccessFlags.STATIC, AccessFlags.CONSTRUCTOR) - custom { method, classDef -> - classDef.type == encoreColorsClassName && - method.containsLiteralInstruction(PLAYLIST_BACKGROUND_COLOR_LITERAL) - } - } - - encoreColorsConstructorFingerprint.method.apply { - // Playlist song list background color. - addColorChangeInstructions(PLAYLIST_BACKGROUND_COLOR_LITERAL, backgroundColor!!) - - // Share menu background color. - addColorChangeInstructions(SHARE_MENU_BACKGROUND_COLOR_LITERAL, backgroundColorSecondary!!) - } - - homeCategoryPillColorsFingerprint.method.apply { - // Home category pills background color. - addColorChangeInstructions(HOME_CATEGORY_PILL_COLOR_LITERAL, backgroundColorSecondary!!) - } - - settingsHeaderColorFingerprint.method.apply { - // Settings header background color. - addColorChangeInstructions(SETTINGS_HEADER_COLOR_LITERAL, backgroundColorSecondary!!) - } - } -} diff --git a/patches/src/main/kotlin/app/revanced/patches/spotify/layout/theme/CustomThemePatch.kt b/patches/src/main/kotlin/app/revanced/patches/spotify/layout/theme/CustomThemePatch.kt index 5f2f09435..da0d8482d 100644 --- a/patches/src/main/kotlin/app/revanced/patches/spotify/layout/theme/CustomThemePatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/spotify/layout/theme/CustomThemePatch.kt @@ -1,8 +1,133 @@ package app.revanced.patches.spotify.layout.theme +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.fingerprint +import app.revanced.patcher.patch.booleanOption +import app.revanced.patcher.patch.bytecodePatch import app.revanced.patcher.patch.resourcePatch +import app.revanced.patcher.patch.stringOption +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod +import app.revanced.patches.spotify.misc.extension.IS_SPOTIFY_LEGACY_APP_TARGET +import app.revanced.patches.spotify.misc.extension.sharedExtensionPatch +import app.revanced.util.* +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.FieldReference import org.w3c.dom.Element +private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/spotify/layout/theme/CustomThemePatch;" + +internal val spotifyBackgroundColor = stringOption( + key = "backgroundColor", + default = "@android:color/black", + title = "Primary background color", + description = "The background color. Can be a hex color or a resource reference.", + required = true, +) + +internal val overridePlayerGradientColor = booleanOption( + key = "overridePlayerGradientColor", + default = false, + title = "Override player gradient color", + description = "Apply primary background color to the player gradient color, which changes dynamically with the song.", + required = false +) + +internal val spotifyBackgroundColorSecondary = stringOption( + key = "backgroundColorSecondary", + default = "#FF121212", + title = "Secondary background color", + description = + "The secondary background color. (e.g. playlist list in home, player artist, song credits). Can be a hex color or a resource reference.", + required = true, +) + +internal val spotifyAccentColor = stringOption( + key = "accentColor", + default = "#FF1ED760", + title = "Accent color", + description = "The accent color ('Spotify green' by default). Can be a hex color or a resource reference.", + required = true, +) + +internal val spotifyAccentColorPressed = stringOption( + key = "accentColorPressed", + default = "#FF169C46", + title = "Pressed dark theme accent color", + description = + "The color when accented buttons are pressed, by default slightly darker than accent. Can be a hex color or a resource reference.", + required = true, +) + +private val customThemeBytecodePatch = bytecodePatch { + dependsOn(sharedExtensionPatch) + + execute { + if (IS_SPOTIFY_LEGACY_APP_TARGET) { + // Bytecode changes are not needed for legacy app target. + // Player background color is changed with existing resource patch. + return@execute + } + + fun MutableMethod.addColorChangeInstructions(literal: Long, colorString: String) { + val index = indexOfFirstLiteralInstructionOrThrow(literal) + val register = getInstruction(index).registerA + + addInstructions( + index + 1, + """ + const-string v$register, "$colorString" + invoke-static { v$register }, $EXTENSION_CLASS_DESCRIPTOR->getThemeColor(Ljava/lang/String;)J + move-result-wide v$register + """ + ) + } + + val encoreColorsClassName = with(encoreThemeFingerprint.originalMethod) { + // "Encore" colors are referenced right before the value of POSITIVE_INFINITY is returned. + // Begin the instruction find using the index of where POSITIVE_INFINITY is set into the register. + val positiveInfinityIndex = indexOfFirstLiteralInstructionOrThrow( + Float.POSITIVE_INFINITY + ) + val encoreColorsFieldReferenceIndex = indexOfFirstInstructionReversedOrThrow( + positiveInfinityIndex, + Opcode.SGET_OBJECT + ) + + getInstruction(encoreColorsFieldReferenceIndex) + .getReference()!!.definingClass + } + + val encoreColorsConstructorFingerprint = fingerprint { + accessFlags(AccessFlags.STATIC, AccessFlags.CONSTRUCTOR) + custom { method, classDef -> + classDef.type == encoreColorsClassName && + method.containsLiteralInstruction(PLAYLIST_BACKGROUND_COLOR_LITERAL) + } + } + + val backgroundColor by spotifyBackgroundColor + val backgroundColorSecondary by spotifyBackgroundColorSecondary + + encoreColorsConstructorFingerprint.method.apply { + addColorChangeInstructions(PLAYLIST_BACKGROUND_COLOR_LITERAL, backgroundColor!!) + addColorChangeInstructions(SHARE_MENU_BACKGROUND_COLOR_LITERAL, backgroundColorSecondary!!) + } + + homeCategoryPillColorsFingerprint.method.addColorChangeInstructions( + HOME_CATEGORY_PILL_COLOR_LITERAL, + backgroundColorSecondary!! + ) + + settingsHeaderColorFingerprint.method.addColorChangeInstructions( + SETTINGS_HEADER_COLOR_LITERAL, + backgroundColorSecondary!! + ) + } +} + @Suppress("unused") val customThemePatch = resourcePatch( name = "Custom theme", @@ -11,9 +136,10 @@ val customThemePatch = resourcePatch( ) { compatibleWith("com.spotify.music") - dependsOn(customThemeByteCodePatch) + dependsOn(customThemeBytecodePatch) val backgroundColor by spotifyBackgroundColor() + val overridePlayerGradientColor by overridePlayerGradientColor() val backgroundColorSecondary by spotifyBackgroundColorSecondary() val accentColor by spotifyAccentColor() val accentColorPressed by spotifyAccentColorPressed() @@ -25,31 +151,39 @@ val customThemePatch = resourcePatch( val childNodes = resourcesNode.childNodes for (i in 0 until childNodes.length) { val node = childNodes.item(i) as? Element ?: continue + val name = node.getAttribute("name") - node.textContent = when (node.getAttribute("name")) { - // Gradient next to user photo and "All" in home page + // Skip overriding song/player gradient start color if the option is disabled. + // Gradient end color should be themed regardless to allow the gradient to connect with + // our primary background color. + if (name == "bg_gradient_start_color" && !overridePlayerGradientColor!!) { + continue + } + + node.textContent = when (name) { + // Gradient next to user photo and "All" in home page. "dark_base_background_base", - // Main background + // Main background. "gray_7", - // Left sidebar background in tablet mode + // Left sidebar background in tablet mode. "gray_10", - // Add account, Settings and privacy, View Profile left sidebar background + // "Add account", "Settings and privacy", "View Profile" left sidebar background. "dark_base_background_elevated_base", - // Song/player background + // Song/player gradient start/end color. "bg_gradient_start_color", "bg_gradient_end_color", - // Login screen - "sthlm_blk", "sthlm_blk_grad_start", "stockholm_black", - // Misc + // Login screen background and gradient start. + "sthlm_blk", "sthlm_blk_grad_start", + // Misc. "image_placeholder_color", -> backgroundColor - // Track credits, merch in song player + // Track credits, merch background in song player. "track_credits_card_bg", "benefit_list_default_color", "merch_card_background", - // Playlist list background in home page + // Playlist list background in home page. "opacity_white_10", - // About artist background in song player + // "About the artist" background in song player. "gray_15", - // What's New pills background + // "What's New" pills background. "dark_base_background_tinted_highlight" -> backgroundColorSecondary @@ -59,5 +193,13 @@ val customThemePatch = resourcePatch( } } } + + // Login screen gradient. + document("res/drawable/start_screen_gradient.xml").use { document -> + val gradientNode = document.getElementsByTagName("gradient").item(0) as Element + + gradientNode.setAttribute("android:startColor", backgroundColor) + gradientNode.setAttribute("android:endColor", backgroundColor) + } } } diff --git a/patches/src/main/kotlin/app/revanced/patches/spotify/layout/theme/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/spotify/layout/theme/Fingerprints.kt index 28943e040..ce4840283 100644 --- a/patches/src/main/kotlin/app/revanced/patches/spotify/layout/theme/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/spotify/layout/theme/Fingerprints.kt @@ -5,13 +5,13 @@ import app.revanced.util.containsLiteralInstruction import com.android.tools.smali.dexlib2.AccessFlags internal val encoreThemeFingerprint = fingerprint { - strings("Encore theme was not provided.") // Partial string match. + strings("No EncoreLayoutTheme provided") } -internal const val SETTINGS_HEADER_COLOR_LITERAL = 0xFF282828 -internal const val HOME_CATEGORY_PILL_COLOR_LITERAL = 0xFF333333 internal const val PLAYLIST_BACKGROUND_COLOR_LITERAL = 0xFF121212 internal const val SHARE_MENU_BACKGROUND_COLOR_LITERAL = 0xFF1F1F1F +internal const val HOME_CATEGORY_PILL_COLOR_LITERAL = 0xFF333333 +internal const val SETTINGS_HEADER_COLOR_LITERAL = 0xFF282828 internal val homeCategoryPillColorsFingerprint = fingerprint{ accessFlags(AccessFlags.STATIC, AccessFlags.CONSTRUCTOR) diff --git a/patches/src/main/kotlin/app/revanced/patches/spotify/layout/theme/Options.kt b/patches/src/main/kotlin/app/revanced/patches/spotify/layout/theme/Options.kt deleted file mode 100644 index e71c97912..000000000 --- a/patches/src/main/kotlin/app/revanced/patches/spotify/layout/theme/Options.kt +++ /dev/null @@ -1,36 +0,0 @@ -package app.revanced.patches.spotify.layout.theme - -import app.revanced.patcher.patch.stringOption - -internal val spotifyBackgroundColor = stringOption( - key = "backgroundColor", - default = "@android:color/black", - title = "Primary background color", - description = "The background color. Can be a hex color or a resource reference.", - required = true, -) - -internal val spotifyBackgroundColorSecondary = stringOption( - key = "backgroundColorSecondary", - default = "#FF121212", - title = "Secondary background color", - description = "The secondary background color. (e.g. playlist list, player arist, credits). Can be a hex color or a resource reference.", - required = true, -) - -internal val spotifyAccentColor = stringOption( - key = "accentColor", - default = "#FF1ED760", - title = "Accent color", - description = "The accent color ('Spotify green' by default). Can be a hex color or a resource reference.", - required = true, -) - -internal val spotifyAccentColorPressed = stringOption( - key = "accentColorPressed", - default = "#FF169C46", - title = "Pressed dark theme accent color", - description = - "The color when accented buttons are pressed, by default slightly darker than accent. Can be a hex color or a resource reference.", - required = true, -) diff --git a/patches/src/main/kotlin/app/revanced/patches/spotify/misc/UnlockPremiumPatch.kt b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/UnlockPremiumPatch.kt index 57d95e1e7..8678517f9 100644 --- a/patches/src/main/kotlin/app/revanced/patches/spotify/misc/UnlockPremiumPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/UnlockPremiumPatch.kt @@ -49,7 +49,7 @@ val unlockPremiumPatch = bytecodePatch( } // Override the attributes map in the getter method. - with(productStateProtoFingerprint.method) { + productStateProtoFingerprint.method.apply { val getAttributesMapIndex = indexOfFirstInstructionOrThrow(Opcode.IGET_OBJECT) val attributesMapRegister = getInstruction(getAttributesMapIndex).registerA @@ -62,10 +62,11 @@ val unlockPremiumPatch = bytecodePatch( // Add the query parameter trackRows to show popular tracks in the artist page. - with(buildQueryParametersFingerprint) { + buildQueryParametersFingerprint.apply { val addQueryParameterConditionIndex = method.indexOfFirstInstructionReversedOrThrow( stringMatches!!.first().index, Opcode.IF_EQZ ) + method.replaceInstruction(addQueryParameterConditionIndex, "nop") } @@ -114,17 +115,19 @@ val unlockPremiumPatch = bytecodePatch( // Disable the "Spotify Premium" upsell experiment in context menus. - with(contextMenuExperimentsFingerprint) { + contextMenuExperimentsFingerprint.apply { val moveIsEnabledIndex = method.indexOfFirstInstructionOrThrow( stringMatches!!.first().index, Opcode.MOVE_RESULT ) val isUpsellEnabledRegister = method.getInstruction(moveIsEnabledIndex).registerA + method.replaceInstruction(moveIsEnabledIndex, "const/4 v$isUpsellEnabledRegister, 0") } // Make featureTypeCase_ accessible so we can check the home section type in the extension. homeSectionFingerprint.classDef.fields.first { it.name == "featureTypeCase_" }.apply { + // Add public flag and remove private. accessFlags = accessFlags.or(AccessFlags.PUBLIC.value).and(AccessFlags.PRIVATE.value.inv()) } @@ -143,7 +146,7 @@ val unlockPremiumPatch = bytecodePatch( // Protobuffer list has an 'isMutable' boolean parameter that sets the mutability. // Forcing that always on breaks unrelated code in strange ways. // Instead, remove the method call that checks if the list is unmodifiable. - with(protobufListRemoveFingerprint.method) { + protobufListRemoveFingerprint.method.apply { val invokeThrowUnmodifiableIndex = indexOfFirstInstructionOrThrow { val reference = getReference() opcode == Opcode.INVOKE_VIRTUAL && @@ -155,7 +158,7 @@ val unlockPremiumPatch = bytecodePatch( } // Remove ads sections from home. - with(homeStructureFingerprint.method) { + homeStructureFingerprint.method.apply { val getSectionsIndex = indexOfFirstInstructionOrThrow(Opcode.IGET_OBJECT) val sectionsRegister = getInstruction(getSectionsIndex).registerA diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/speed/custom/CustomPlaybackSpeedPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/speed/custom/CustomPlaybackSpeedPatch.kt index 53151489b..d6a7fb561 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/video/speed/custom/CustomPlaybackSpeedPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/speed/custom/CustomPlaybackSpeedPatch.kt @@ -121,11 +121,11 @@ internal val customPlaybackSpeedPatch = bytecodePatch( // Override the min/max speeds that can be used. speedLimiterFingerprint.method.apply { - val limitMinIndex = indexOfFirstLiteralInstructionOrThrow(0.25f.toRawBits().toLong()) - var limitMaxIndex = indexOfFirstLiteralInstruction(2.0f.toRawBits().toLong()) + val limitMinIndex = indexOfFirstLiteralInstructionOrThrow(0.25f) + var limitMaxIndex = indexOfFirstLiteralInstruction(2.0f) // Newer targets have 4x max speed. if (limitMaxIndex < 0) { - limitMaxIndex = indexOfFirstLiteralInstructionOrThrow(4.0f.toRawBits().toLong()) + limitMaxIndex = indexOfFirstLiteralInstructionOrThrow(4.0f) } val limitMinRegister = getInstruction(limitMinIndex).registerA diff --git a/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt b/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt index 4d3826865..319f6b0b7 100644 --- a/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt +++ b/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt @@ -14,6 +14,9 @@ import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod import app.revanced.patches.shared.misc.mapping.get import app.revanced.patches.shared.misc.mapping.resourceMappingPatch import app.revanced.patches.shared.misc.mapping.resourceMappings +import app.revanced.util.InstructionUtils.Companion.branchOpcodes +import app.revanced.util.InstructionUtils.Companion.returnOpcodes +import app.revanced.util.InstructionUtils.Companion.writeOpcodes import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.Opcode.* import com.android.tools.smali.dexlib2.iface.Method @@ -43,7 +46,7 @@ import java.util.EnumSet * @throws IllegalArgumentException If a branch or conditional statement is encountered * before a suitable register is found. */ -internal fun Method.findFreeRegister(startIndex: Int, vararg registersToExclude: Int): Int { +fun Method.findFreeRegister(startIndex: Int, vararg registersToExclude: Int): Int { if (implementation == null) { throw IllegalArgumentException("Method has no implementation: $this") } @@ -51,82 +54,6 @@ internal fun Method.findFreeRegister(startIndex: Int, vararg registersToExclude: throw IllegalArgumentException("startIndex out of bounds: $startIndex") } - // All registers used by an instruction. - fun Instruction.getRegistersUsed() = when (this) { - is FiveRegisterInstruction -> { - when (registerCount) { - 1 -> listOf(registerC) - 2 -> listOf(registerC, registerD) - 3 -> listOf(registerC, registerD, registerE) - 4 -> listOf(registerC, registerD, registerE, registerF) - else -> listOf(registerC, registerD, registerE, registerF, registerG) - } - } - is ThreeRegisterInstruction -> listOf(registerA, registerB, registerC) - is TwoRegisterInstruction -> listOf(registerA, registerB) - is OneRegisterInstruction -> listOf(registerA) - is RegisterRangeInstruction -> (startRegister until (startRegister + registerCount)).toList() - else -> emptyList() - } - - // Register that is written to by an instruction. - fun Instruction.getWriteRegister() : Int { - // Two and three register instructions extend OneRegisterInstruction. - if (this is OneRegisterInstruction) return registerA - throw IllegalStateException("Not a write instruction: $this") - } - - val writeOpcodes = EnumSet.of( - ARRAY_LENGTH, - INSTANCE_OF, - NEW_INSTANCE, NEW_ARRAY, - MOVE, MOVE_FROM16, MOVE_16, MOVE_WIDE, MOVE_WIDE_FROM16, MOVE_WIDE_16, MOVE_OBJECT, - MOVE_OBJECT_FROM16, MOVE_OBJECT_16, MOVE_RESULT, MOVE_RESULT_WIDE, MOVE_RESULT_OBJECT, MOVE_EXCEPTION, - CONST, CONST_4, CONST_16, CONST_HIGH16, CONST_WIDE_16, CONST_WIDE_32, - CONST_WIDE, CONST_WIDE_HIGH16, CONST_STRING, CONST_STRING_JUMBO, - IGET, IGET_WIDE, IGET_OBJECT, IGET_BOOLEAN, IGET_BYTE, IGET_CHAR, IGET_SHORT, - IGET_VOLATILE, IGET_WIDE_VOLATILE, IGET_OBJECT_VOLATILE, - SGET, SGET_WIDE, SGET_OBJECT, SGET_BOOLEAN, SGET_BYTE, SGET_CHAR, SGET_SHORT, - SGET_VOLATILE, SGET_WIDE_VOLATILE, SGET_OBJECT_VOLATILE, - AGET, AGET_WIDE, AGET_OBJECT, AGET_BOOLEAN, AGET_BYTE, AGET_CHAR, AGET_SHORT, - // Arithmetic and logical operations. - ADD_DOUBLE_2ADDR, ADD_DOUBLE, ADD_FLOAT_2ADDR, ADD_FLOAT, ADD_INT_2ADDR, - ADD_INT_LIT8, ADD_INT, ADD_LONG_2ADDR, ADD_LONG, ADD_INT_LIT16, - AND_INT_2ADDR, AND_INT_LIT8, AND_INT_LIT16, AND_INT, AND_LONG_2ADDR, AND_LONG, - DIV_DOUBLE_2ADDR, DIV_DOUBLE, DIV_FLOAT_2ADDR, DIV_FLOAT, DIV_INT_2ADDR, - DIV_INT_LIT16, DIV_INT_LIT8, DIV_INT, DIV_LONG_2ADDR, DIV_LONG, - DOUBLE_TO_FLOAT, DOUBLE_TO_INT, DOUBLE_TO_LONG, - FLOAT_TO_DOUBLE, FLOAT_TO_INT, FLOAT_TO_LONG, - INT_TO_BYTE, INT_TO_CHAR, INT_TO_DOUBLE, INT_TO_FLOAT, INT_TO_LONG, INT_TO_SHORT, - LONG_TO_DOUBLE, LONG_TO_FLOAT, LONG_TO_INT, - MUL_DOUBLE_2ADDR, MUL_DOUBLE, MUL_FLOAT_2ADDR, MUL_FLOAT, MUL_INT_2ADDR, - MUL_INT_LIT16, MUL_INT_LIT8, MUL_INT, MUL_LONG_2ADDR, MUL_LONG, - NEG_DOUBLE, NEG_FLOAT, NEG_INT, NEG_LONG, - NOT_INT, NOT_LONG, - OR_INT_2ADDR, OR_INT_LIT16, OR_INT_LIT8, OR_INT, OR_LONG_2ADDR, OR_LONG, - REM_DOUBLE_2ADDR, REM_DOUBLE, REM_FLOAT_2ADDR, REM_FLOAT, REM_INT_2ADDR, - REM_INT_LIT16, REM_INT_LIT8, REM_INT, REM_LONG_2ADDR, REM_LONG, - RSUB_INT_LIT8, RSUB_INT, - SHL_INT_2ADDR, SHL_INT_LIT8, SHL_INT, SHL_LONG_2ADDR, SHL_LONG, - SHR_INT_2ADDR, SHR_INT_LIT8, SHR_INT, SHR_LONG_2ADDR, SHR_LONG, - SUB_DOUBLE_2ADDR, SUB_DOUBLE, SUB_FLOAT_2ADDR, SUB_FLOAT, SUB_INT_2ADDR, - SUB_INT, SUB_LONG_2ADDR, SUB_LONG, - USHR_INT_2ADDR, USHR_INT_LIT8, USHR_INT, USHR_LONG_2ADDR, USHR_LONG, - XOR_INT_2ADDR, XOR_INT_LIT16, XOR_INT_LIT8, XOR_INT, XOR_LONG_2ADDR, XOR_LONG, - ) - - val branchOpcodes = EnumSet.of( - GOTO, GOTO_16, GOTO_32, - IF_EQ, IF_NE, IF_LT, IF_GE, IF_GT, IF_LE, - IF_EQZ, IF_NEZ, IF_LTZ, IF_GEZ, IF_GTZ, IF_LEZ, - PACKED_SWITCH_PAYLOAD, SPARSE_SWITCH_PAYLOAD - ) - - val returnOpcodes = EnumSet.of( - RETURN_VOID, RETURN, RETURN_WIDE, RETURN_OBJECT, RETURN_VOID_NO_BARRIER, - THROW - ) - // Highest 4-bit register available, exclusive. Ideally return a free register less than this. val maxRegister4Bits = 16 var bestFreeRegisterFound: Int? = null @@ -134,10 +61,9 @@ internal fun Method.findFreeRegister(startIndex: Int, vararg registersToExclude: for (i in startIndex until instructions.count()) { val instruction = getInstruction(i) - val instructionRegisters = instruction.getRegistersUsed() + val instructionRegisters = instruction.registersUsed - if (instruction.opcode in returnOpcodes) { - // Method returns. + if (instruction.isReturnInstruction) { usedRegisters.addAll(instructionRegisters) // Use lowest register that hasn't been encountered. @@ -157,7 +83,7 @@ internal fun Method.findFreeRegister(startIndex: Int, vararg registersToExclude: "$startIndex excluding: $registersToExclude") } - if (instruction.opcode in branchOpcodes) { + if (instruction.isBranchInstruction) { if (bestFreeRegisterFound != null) { return bestFreeRegisterFound } @@ -165,9 +91,9 @@ internal fun Method.findFreeRegister(startIndex: Int, vararg registersToExclude: throw IllegalArgumentException("Encountered a branch statement before a free register could be found") } - if (instruction.opcode in writeOpcodes) { - val writeRegister = instruction.getWriteRegister() + val writeRegister = instruction.writeRegister + if (writeRegister != null) { if (writeRegister !in usedRegisters) { // Verify the register is only used for write and not also as a parameter. // If the instruction uses the write register once then it's not also a read register. @@ -194,6 +120,53 @@ internal fun Method.findFreeRegister(startIndex: Int, vararg registersToExclude: throw IllegalArgumentException("Start index is outside the range of normal control flow: $startIndex") } +/** + * @return The registers used by this instruction. + */ +internal val Instruction.registersUsed: List + get() = when (this) { + is FiveRegisterInstruction -> { + when (registerCount) { + 1 -> listOf(registerC) + 2 -> listOf(registerC, registerD) + 3 -> listOf(registerC, registerD, registerE) + 4 -> listOf(registerC, registerD, registerE, registerF) + else -> listOf(registerC, registerD, registerE, registerF, registerG) + } + } + is ThreeRegisterInstruction -> listOf(registerA, registerB, registerC) + is TwoRegisterInstruction -> listOf(registerA, registerB) + is OneRegisterInstruction -> listOf(registerA) + is RegisterRangeInstruction -> (startRegister until (startRegister + registerCount)).toList() + else -> emptyList() + } + +/** + * @return The register that is written to by this instruction, + * or NULL if this is not a write opcode. + */ +internal val Instruction.writeRegister: Int? + get() { + if (this.opcode !in writeOpcodes) { + return null + } + if (this !is OneRegisterInstruction) { + throw IllegalStateException("Not a write instruction: $this") + } + return registerA + } + +/** + * @return If this instruction is an unconditional or conditional branch opcode. + */ +internal val Instruction.isBranchInstruction: Boolean + get() = this.opcode in branchOpcodes + +/** + * @return If this instruction returns or throws. + */ +internal val Instruction.isReturnInstruction: Boolean + get() = this.opcode in returnOpcodes /** * Find the [MutableMethod] from a given [Method] in a [MutableClass]. @@ -247,7 +220,7 @@ fun MutableMethod.injectHideViewCall( * (patch code) * (original code) */ -internal fun MutableMethod.addInstructionsAtControlFlowLabel( +fun MutableMethod.addInstructionsAtControlFlowLabel( insertIndex: Int, instructions: String, ) { @@ -298,7 +271,7 @@ fun Method.indexOfFirstResourceIdOrThrow(resourceName: String): Int { } /** - * Find the index of the first literal instruction with the given value. + * Find the index of the first literal instruction with the given long value. * * @return the first literal instruction with the value, or -1 if not found. * @see indexOfFirstLiteralInstructionOrThrow @@ -310,14 +283,56 @@ fun Method.indexOfFirstLiteralInstruction(literal: Long) = implementation?.let { } ?: -1 /** - * Find the index of the first literal instruction with the given value, + * Find the index of the first literal instruction with the given long value, * or throw an exception if not found. * * @return the first literal instruction with the value, or throws [PatchException] if not found. */ fun Method.indexOfFirstLiteralInstructionOrThrow(literal: Long): Int { val index = indexOfFirstLiteralInstruction(literal) - if (index < 0) throw PatchException("Could not find literal value: $literal") + if (index < 0) throw PatchException("Could not find long literal: $literal") + return index +} + +/** + * Find the index of the first literal instruction with the given float value. + * + * @return the first literal instruction with the value, or -1 if not found. + * @see indexOfFirstLiteralInstructionOrThrow + */ +fun Method.indexOfFirstLiteralInstruction(literal: Float) = + indexOfFirstLiteralInstruction(literal.toRawBits().toLong()) + +/** + * Find the index of the first literal instruction with the given float value, + * or throw an exception if not found. + * + * @return the first literal instruction with the value, or throws [PatchException] if not found. + */ +fun Method.indexOfFirstLiteralInstructionOrThrow(literal: Float): Int { + val index = indexOfFirstLiteralInstruction(literal) + if (index < 0) throw PatchException("Could not find float literal: $literal") + return index +} + +/** + * Find the index of the first literal instruction with the given double value. + * + * @return the first literal instruction with the value, or -1 if not found. + * @see indexOfFirstLiteralInstructionOrThrow + */ +fun Method.indexOfFirstLiteralInstruction(literal: Double) = + indexOfFirstLiteralInstruction(literal.toRawBits().toLong()) + +/** + * Find the index of the first literal instruction with the given double value, + * or throw an exception if not found. + * + * @return the first literal instruction with the value, or throws [PatchException] if not found. + */ +fun Method.indexOfFirstLiteralInstructionOrThrow(literal: Double): Int { + val index = indexOfFirstLiteralInstruction(literal) + if (index < 0) throw PatchException("Could not find double literal: $literal") return index } @@ -334,24 +349,80 @@ fun Method.indexOfFirstLiteralInstructionReversed(literal: Long) = implementatio } ?: -1 /** - * Find the index of the last wide literal instruction with the given value, + * Find the index of the last wide literal instruction with the given long value, * or throw an exception if not found. * * @return the last literal instruction with the value, or throws [PatchException] if not found. */ fun Method.indexOfFirstLiteralInstructionReversedOrThrow(literal: Long): Int { val index = indexOfFirstLiteralInstructionReversed(literal) - if (index < 0) throw PatchException("Could not find literal value: $literal") + if (index < 0) throw PatchException("Could not find long literal: $literal") return index } /** - * Check if the method contains a literal with the given value. + * Find the index of the last literal instruction with the given float value. + * + * @return the last literal instruction with the value, or -1 if not found. + * @see indexOfFirstLiteralInstructionOrThrow + */ +fun Method.indexOfFirstLiteralInstructionReversed(literal: Float) = + indexOfFirstLiteralInstructionReversed(literal.toRawBits().toLong()) + +/** + * Find the index of the last wide literal instruction with the given float value, + * or throw an exception if not found. + * + * @return the last literal instruction with the value, or throws [PatchException] if not found. + */ +fun Method.indexOfFirstLiteralInstructionReversedOrThrow(literal: Float): Int { + val index = indexOfFirstLiteralInstructionReversed(literal) + if (index < 0) throw PatchException("Could not find float literal: $literal") + return index +} + +/** + * Find the index of the last literal instruction with the given double value. + * + * @return the last literal instruction with the value, or -1 if not found. + * @see indexOfFirstLiteralInstructionOrThrow + */ +fun Method.indexOfFirstLiteralInstructionReversed(literal: Double) = + indexOfFirstLiteralInstructionReversed(literal.toRawBits().toLong()) + +/** + * Find the index of the last wide literal instruction with the given double value, + * or throw an exception if not found. + * + * @return the last literal instruction with the value, or throws [PatchException] if not found. + */ +fun Method.indexOfFirstLiteralInstructionReversedOrThrow(literal: Double): Int { + val index = indexOfFirstLiteralInstructionReversed(literal) + if (index < 0) throw PatchException("Could not find double literal: $literal") + return index +} + +/** + * Check if the method contains a literal with the given long value. * * @return if the method contains a literal with the given value. */ fun Method.containsLiteralInstruction(literal: Long) = indexOfFirstLiteralInstruction(literal) >= 0 +/** + * Check if the method contains a literal with the given float value. + * + * @return if the method contains a literal with the given value. + */ +fun Method.containsLiteralInstruction(literal: Float) = indexOfFirstLiteralInstruction(literal) >= 0 + +/** + * Check if the method contains a literal with the given double value. + * + * @return if the method contains a literal with the given value. + */ +fun Method.containsLiteralInstruction(literal: Double) = indexOfFirstLiteralInstruction(literal) >= 0 + /** * Traverse the class hierarchy starting from the given root class. * @@ -643,3 +714,58 @@ fun FingerprintBuilder.literal(literalSupplier: () -> Long) { method.containsLiteralInstruction(literalSupplier()) } } + +private class InstructionUtils { + companion object { + val branchOpcodes: EnumSet = EnumSet.of( + GOTO, GOTO_16, GOTO_32, + IF_EQ, IF_NE, IF_LT, IF_GE, IF_GT, IF_LE, + IF_EQZ, IF_NEZ, IF_LTZ, IF_GEZ, IF_GTZ, IF_LEZ, + PACKED_SWITCH_PAYLOAD, SPARSE_SWITCH_PAYLOAD + ) + + val returnOpcodes: EnumSet = EnumSet.of( + RETURN_VOID, RETURN, RETURN_WIDE, RETURN_OBJECT, RETURN_VOID_NO_BARRIER, + THROW + ) + + val writeOpcodes: EnumSet = EnumSet.of( + ARRAY_LENGTH, + INSTANCE_OF, + NEW_INSTANCE, NEW_ARRAY, + MOVE, MOVE_FROM16, MOVE_16, MOVE_WIDE, MOVE_WIDE_FROM16, MOVE_WIDE_16, MOVE_OBJECT, + MOVE_OBJECT_FROM16, MOVE_OBJECT_16, MOVE_RESULT, MOVE_RESULT_WIDE, MOVE_RESULT_OBJECT, MOVE_EXCEPTION, + CONST, CONST_4, CONST_16, CONST_HIGH16, CONST_WIDE_16, CONST_WIDE_32, + CONST_WIDE, CONST_WIDE_HIGH16, CONST_STRING, CONST_STRING_JUMBO, + IGET, IGET_WIDE, IGET_OBJECT, IGET_BOOLEAN, IGET_BYTE, IGET_CHAR, IGET_SHORT, + IGET_VOLATILE, IGET_WIDE_VOLATILE, IGET_OBJECT_VOLATILE, + SGET, SGET_WIDE, SGET_OBJECT, SGET_BOOLEAN, SGET_BYTE, SGET_CHAR, SGET_SHORT, + SGET_VOLATILE, SGET_WIDE_VOLATILE, SGET_OBJECT_VOLATILE, + AGET, AGET_WIDE, AGET_OBJECT, AGET_BOOLEAN, AGET_BYTE, AGET_CHAR, AGET_SHORT, + // Arithmetic and logical operations. + ADD_DOUBLE_2ADDR, ADD_DOUBLE, ADD_FLOAT_2ADDR, ADD_FLOAT, ADD_INT_2ADDR, + ADD_INT_LIT8, ADD_INT, ADD_LONG_2ADDR, ADD_LONG, ADD_INT_LIT16, + AND_INT_2ADDR, AND_INT_LIT8, AND_INT_LIT16, AND_INT, AND_LONG_2ADDR, AND_LONG, + DIV_DOUBLE_2ADDR, DIV_DOUBLE, DIV_FLOAT_2ADDR, DIV_FLOAT, DIV_INT_2ADDR, + DIV_INT_LIT16, DIV_INT_LIT8, DIV_INT, DIV_LONG_2ADDR, DIV_LONG, + DOUBLE_TO_FLOAT, DOUBLE_TO_INT, DOUBLE_TO_LONG, + FLOAT_TO_DOUBLE, FLOAT_TO_INT, FLOAT_TO_LONG, + INT_TO_BYTE, INT_TO_CHAR, INT_TO_DOUBLE, INT_TO_FLOAT, INT_TO_LONG, INT_TO_SHORT, + LONG_TO_DOUBLE, LONG_TO_FLOAT, LONG_TO_INT, + MUL_DOUBLE_2ADDR, MUL_DOUBLE, MUL_FLOAT_2ADDR, MUL_FLOAT, MUL_INT_2ADDR, + MUL_INT_LIT16, MUL_INT_LIT8, MUL_INT, MUL_LONG_2ADDR, MUL_LONG, + NEG_DOUBLE, NEG_FLOAT, NEG_INT, NEG_LONG, + NOT_INT, NOT_LONG, + OR_INT_2ADDR, OR_INT_LIT16, OR_INT_LIT8, OR_INT, OR_LONG_2ADDR, OR_LONG, + REM_DOUBLE_2ADDR, REM_DOUBLE, REM_FLOAT_2ADDR, REM_FLOAT, REM_INT_2ADDR, + REM_INT_LIT16, REM_INT_LIT8, REM_INT, REM_LONG_2ADDR, REM_LONG, + RSUB_INT_LIT8, RSUB_INT, + SHL_INT_2ADDR, SHL_INT_LIT8, SHL_INT, SHL_LONG_2ADDR, SHL_LONG, + SHR_INT_2ADDR, SHR_INT_LIT8, SHR_INT, SHR_LONG_2ADDR, SHR_LONG, + SUB_DOUBLE_2ADDR, SUB_DOUBLE, SUB_FLOAT_2ADDR, SUB_FLOAT, SUB_INT_2ADDR, + SUB_INT, SUB_LONG_2ADDR, SUB_LONG, + USHR_INT_2ADDR, USHR_INT_LIT8, USHR_INT, USHR_LONG_2ADDR, USHR_LONG, + XOR_INT_2ADDR, XOR_INT_LIT16, XOR_INT_LIT8, XOR_INT, XOR_LONG_2ADDR, XOR_LONG, + ) + } +}