From b9ef6ea3bffd54ee63aee1b9e3db795e6d0451d8 Mon Sep 17 00:00:00 2001 From: inotia00 <108592928+inotia00@users.noreply.github.com> Date: Tue, 11 Jun 2024 01:51:16 +0900 Subject: [PATCH] fix(YouTube/Spoof client): restore playback speed menu when spoofing to an iOS, Android TV, Android Testsuite client https://github.com/ReVanced/revanced-patches/commit/95f290f1139cc8679beecac53c623847668f885e --- .../patches/shared/litho/LithoFilterPatch.kt | 4 +- .../spoofuseragent/BaseSpoofUserAgentPatch.kt | 70 +++++++-------- ...aybackRateBottomSheetBuilderFingerprint.kt | 18 ++++ .../utils/fix/client/SpoofClientPatch.kt | 85 +++++++++++++++---- .../CreatePlaybackSpeedMenuItemFingerprint.kt | 34 ++++++++ ...PlayerGestureConfigSyntheticFingerprint.kt | 32 ++++--- .../utils/flyoutmenu/FlyoutMenuHookPatch.kt | 14 +-- ...PlaybackRateBottomSheetClassFingerprint.kt | 12 --- .../utils/resourceid/SharedResourceIdPatch.kt | 2 + .../kotlin/app/revanced/util/BytecodeUtils.kt | 69 ++++++++++++++- .../youtube/settings/host/values/strings.xml | 7 +- 11 files changed, 250 insertions(+), 97 deletions(-) create mode 100644 src/main/kotlin/app/revanced/patches/youtube/utils/fingerprints/PlaybackRateBottomSheetBuilderFingerprint.kt create mode 100644 src/main/kotlin/app/revanced/patches/youtube/utils/fix/client/fingerprints/CreatePlaybackSpeedMenuItemFingerprint.kt delete mode 100644 src/main/kotlin/app/revanced/patches/youtube/utils/flyoutmenu/fingerprints/PlaybackRateBottomSheetClassFingerprint.kt diff --git a/src/main/kotlin/app/revanced/patches/shared/litho/LithoFilterPatch.kt b/src/main/kotlin/app/revanced/patches/shared/litho/LithoFilterPatch.kt index 9c329bc72..ebd13581e 100644 --- a/src/main/kotlin/app/revanced/patches/shared/litho/LithoFilterPatch.kt +++ b/src/main/kotlin/app/revanced/patches/shared/litho/LithoFilterPatch.kt @@ -14,7 +14,7 @@ import app.revanced.patches.shared.litho.fingerprints.EmptyComponentsFingerprint import app.revanced.patches.shared.litho.fingerprints.LithoFilterPatchConstructorFingerprint import app.revanced.patches.shared.litho.fingerprints.PathBuilderFingerprint import app.revanced.patches.shared.litho.fingerprints.SetByteBufferFingerprint -import app.revanced.util.getEmptyStringInstructionIndex +import app.revanced.util.getStringInstructionIndex import app.revanced.util.getTargetIndex import app.revanced.util.getTargetIndexReversed import app.revanced.util.getTargetIndexWithFieldReferenceType @@ -140,7 +140,7 @@ object LithoFilterPatch : BytecodePatch( val stringBuilderRegister = getInstruction(stringBuilderIndex).registerA - val emptyStringIndex = getEmptyStringInstructionIndex() + val emptyStringIndex = getStringInstructionIndex("") val identifierIndex = getTargetIndexReversed(emptyStringIndex, Opcode.IPUT_OBJECT) val identifierRegister = diff --git a/src/main/kotlin/app/revanced/patches/shared/spoofuseragent/BaseSpoofUserAgentPatch.kt b/src/main/kotlin/app/revanced/patches/shared/spoofuseragent/BaseSpoofUserAgentPatch.kt index 20dab60e4..f1ae927cb 100644 --- a/src/main/kotlin/app/revanced/patches/shared/spoofuseragent/BaseSpoofUserAgentPatch.kt +++ b/src/main/kotlin/app/revanced/patches/shared/spoofuseragent/BaseSpoofUserAgentPatch.kt @@ -8,13 +8,14 @@ import app.revanced.patches.shared.transformation.IMethodCall import app.revanced.patches.shared.transformation.Instruction35cInfo import app.revanced.patches.shared.transformation.filterMapInstruction35c import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstruction import com.android.tools.smali.dexlib2.Opcode -import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction21c import com.android.tools.smali.dexlib2.iface.ClassDef import com.android.tools.smali.dexlib2.iface.Method import com.android.tools.smali.dexlib2.iface.instruction.Instruction import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction import com.android.tools.smali.dexlib2.iface.reference.MethodReference +import com.android.tools.smali.dexlib2.iface.reference.StringReference abstract class BaseSpoofUserAgentPatch( private val packageName: String @@ -36,45 +37,39 @@ abstract class BaseSpoofUserAgentPatch( // Replace the result of context.getPackageName(), if it is used in a user agent string. mutableMethod.apply { - var isTargetMethod = true + // After context.getPackageName() the result is moved to a register. + val targetRegister = ( + getInstruction(instructionIndex + 1) + as? OneRegisterInstruction ?: return + ).registerA - for ((index, instruction) in implementation!!.instructions.withIndex()) { - if (instruction.opcode != Opcode.CONST_STRING) - continue + // IndexOutOfBoundsException is possible here, + // but no such occurrences are present in the app. + val referee = + getInstruction(instructionIndex + 2).getReference()?.toString() - val constString = getInstruction(index).reference.toString() - - if (constString != "android.resource://" && constString != "gcore_") - continue - - isTargetMethod = false - break + // Only replace string builder usage. + if (referee != USER_AGENT_STRING_BUILDER_APPEND_METHOD_REFERENCE) { + return } - if (isTargetMethod) { - // After context.getPackageName() the result is moved to a register. - val targetRegister = ( - getInstruction(instructionIndex + 1) - as? OneRegisterInstruction ?: return - ).registerA - - // IndexOutOfBoundsException is not possible here, - // but no such occurrences are present in the app. - val referee = - getInstruction(instructionIndex + 2).getReference()?.toString() - - // This can technically also match non-user agent string builder append methods, - // but no such occurrences are present in the app. - if (referee != "Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;") { - return - } - - // Overwrite the result of context.getPackageName() with the original package name. - replaceInstruction( - instructionIndex + 1, - "const-string v$targetRegister, \"$packageName\"", - ) + // Do not change the package name in methods that use resources, or for methods that use GmsCore. + // Changing these package names will result in playback limitations, + // particularly Android VR background audio only playback. + val resourceOrGmsStringInstructionIndex = indexOfFirstInstruction { + val reference = getReference() + opcode == Opcode.CONST_STRING && + (reference?.string == "android.resource://" || reference?.string == "gcore_") } + if (resourceOrGmsStringInstructionIndex >= 0) { + return + } + + // Overwrite the result of context.getPackageName() with the original package name. + replaceInstruction( + instructionIndex + 1, + "const-string v$targetRegister, \"$packageName\"", + ) } } @@ -92,4 +87,9 @@ abstract class BaseSpoofUserAgentPatch( "Ljava/lang/String;", ), } + + private companion object { + private const val USER_AGENT_STRING_BUILDER_APPEND_METHOD_REFERENCE = + "Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;" + } } \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patches/youtube/utils/fingerprints/PlaybackRateBottomSheetBuilderFingerprint.kt b/src/main/kotlin/app/revanced/patches/youtube/utils/fingerprints/PlaybackRateBottomSheetBuilderFingerprint.kt new file mode 100644 index 000000000..24dc36eb6 --- /dev/null +++ b/src/main/kotlin/app/revanced/patches/youtube/utils/fingerprints/PlaybackRateBottomSheetBuilderFingerprint.kt @@ -0,0 +1,18 @@ +package app.revanced.patches.youtube.utils.fingerprints + +import app.revanced.patcher.extensions.or +import app.revanced.patches.youtube.utils.resourceid.SharedResourceIdPatch.VarispeedUnavailableTitle +import app.revanced.util.fingerprint.LiteralValueFingerprint +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal object PlaybackRateBottomSheetBuilderFingerprint : LiteralValueFingerprint( + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = emptyList(), + opcodes = listOf( + Opcode.IGET_BOOLEAN, + Opcode.IF_EQZ, + ), + literalSupplier = { VarispeedUnavailableTitle } +) \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patches/youtube/utils/fix/client/SpoofClientPatch.kt b/src/main/kotlin/app/revanced/patches/youtube/utils/fix/client/SpoofClientPatch.kt index c4db670bf..dd1505458 100644 --- a/src/main/kotlin/app/revanced/patches/youtube/utils/fix/client/SpoofClientPatch.kt +++ b/src/main/kotlin/app/revanced/patches/youtube/utils/fix/client/SpoofClientPatch.kt @@ -11,20 +11,23 @@ import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMu import app.revanced.patches.shared.fingerprints.CreatePlayerRequestBodyWithModelFingerprint import app.revanced.patches.shared.fingerprints.CreatePlayerRequestBodyWithModelFingerprint.indexOfModelInstruction import app.revanced.patches.youtube.utils.compatibility.Constants +import app.revanced.patches.youtube.utils.fingerprints.PlaybackRateBottomSheetBuilderFingerprint import app.revanced.patches.youtube.utils.fix.client.fingerprints.BuildInitPlaybackRequestFingerprint import app.revanced.patches.youtube.utils.fix.client.fingerprints.BuildPlayerRequestURIFingerprint +import app.revanced.patches.youtube.utils.fix.client.fingerprints.CreatePlaybackSpeedMenuItemFingerprint import app.revanced.patches.youtube.utils.fix.client.fingerprints.CreatePlayerRequestBodyFingerprint import app.revanced.patches.youtube.utils.fix.client.fingerprints.NerdsStatsVideoFormatBuilderFingerprint import app.revanced.patches.youtube.utils.fix.client.fingerprints.PlayerGestureConfigSyntheticFingerprint import app.revanced.patches.youtube.utils.fix.client.fingerprints.SetPlayerRequestClientTypeFingerprint import app.revanced.patches.youtube.utils.integrations.Constants.MISC_PATH -import app.revanced.patches.youtube.utils.playertype.PlayerTypeHookPatch +import app.revanced.patches.youtube.utils.resourceid.SharedResourceIdPatch import app.revanced.patches.youtube.utils.settings.SettingsPatch import app.revanced.patches.youtube.video.information.VideoInformationPatch import app.revanced.patches.youtube.video.playerresponse.PlayerResponseMethodHookPatch import app.revanced.util.getReference import app.revanced.util.getStringInstructionIndex import app.revanced.util.getWalkerMethod +import app.revanced.util.indexOfFirstInstructionOrThrow import app.revanced.util.patch.BaseBytecodePatch import app.revanced.util.resultOrThrow import com.android.tools.smali.dexlib2.AccessFlags @@ -42,11 +45,11 @@ object SpoofClientPatch : BaseBytecodePatch( name = "Spoof client", description = "Adds options to spoof the client to allow video playback.", dependencies = setOf( - PlayerTypeHookPatch::class, PlayerResponseMethodHookPatch::class, SettingsPatch::class, VideoInformationPatch::class, SpoofUserAgentPatch::class, + SharedResourceIdPatch::class, ), compatiblePackages = Constants.COMPATIBLE_PACKAGE, fingerprints = setOf( @@ -60,6 +63,10 @@ object SpoofClientPatch : BaseBytecodePatch( // Player gesture config. PlayerGestureConfigSyntheticFingerprint, + // Player speed menu item. + CreatePlaybackSpeedMenuItemFingerprint, + PlaybackRateBottomSheetBuilderFingerprint, + // Nerds stats video format. NerdsStatsVideoFormatBuilderFingerprint, ) @@ -257,26 +264,74 @@ object SpoofClientPatch : BaseBytecodePatch( // region fix player gesture. PlayerGestureConfigSyntheticFingerprint.resultOrThrow().let { - arrayOf(3, 9).forEach { offSet -> - it.getWalkerMethod(context, it.scanResult.patternScanResult!!.endIndex - offSet) - .apply { - val index = implementation!!.instructions.size - 1 - val register = getInstruction(index).registerA + val endIndex = it.scanResult.patternScanResult!!.endIndex + val downAndOutLandscapeAllowedIndex = endIndex - 3 + val downAndOutPortraitAllowedIndex = endIndex - 9 - addInstructions( - index, - """ - invoke-static {v$register}, $INTEGRATIONS_CLASS_DESCRIPTOR->enablePlayerGesture(Z)Z - move-result v$register + arrayOf( + downAndOutLandscapeAllowedIndex, + downAndOutPortraitAllowedIndex, + ).forEach { index -> + val gestureAllowedMethod = it.getWalkerMethod(context, index) + + gestureAllowedMethod.apply { + val isAllowedIndex = getInstructions().lastIndex + val isAllowed = getInstruction(isAllowedIndex).registerA + + addInstructions( + isAllowedIndex, """ - ) - } + invoke-static { v$isAllowed }, $INTEGRATIONS_CLASS_DESCRIPTOR->enablePlayerGesture(Z)Z + move-result v$isAllowed + """, + ) + } } } // endregion - // region append spoof info + // region fix playback speed menu item. + + // fix for iOS, Android Testsuite + CreatePlaybackSpeedMenuItemFingerprint.resultOrThrow().let { + val scanResult = it.scanResult.patternScanResult!! + if (scanResult.startIndex != 0) throw PatchException("Unexpected start index: ${scanResult.startIndex}") + + it.mutableMethod.apply { + // Find the conditional check if the playback speed menu item is not created. + val shouldCreateMenuIndex = indexOfFirstInstructionOrThrow(scanResult.endIndex) { opcode == Opcode.IF_EQZ } + val shouldCreateMenuRegister = getInstruction(shouldCreateMenuIndex).registerA + + addInstructions( + shouldCreateMenuIndex, + """ + invoke-static { v$shouldCreateMenuRegister }, $INTEGRATIONS_CLASS_DESCRIPTOR->forceCreatePlaybackSpeedMenu(Z)Z + move-result v$shouldCreateMenuRegister + """, + ) + } + } + + // fix for Android TV + PlaybackRateBottomSheetBuilderFingerprint.resultOrThrow().let { + it.mutableMethod.apply { + val targetIndex = it.scanResult.patternScanResult!!.endIndex + val targetRegister = getInstruction(targetIndex).registerA + + addInstructions( + targetIndex, + """ + invoke-static { v$targetRegister }, $INTEGRATIONS_CLASS_DESCRIPTOR->forceCreatePlaybackSpeedMenuReversed(Z)Z + move-result v$targetRegister + """, + ) + } + } + + // endregion + + // region append spoof info. NerdsStatsVideoFormatBuilderFingerprint.resultOrThrow().mutableMethod.apply { for (index in implementation!!.instructions.size - 1 downTo 0) { diff --git a/src/main/kotlin/app/revanced/patches/youtube/utils/fix/client/fingerprints/CreatePlaybackSpeedMenuItemFingerprint.kt b/src/main/kotlin/app/revanced/patches/youtube/utils/fix/client/fingerprints/CreatePlaybackSpeedMenuItemFingerprint.kt new file mode 100644 index 000000000..0e79e5440 --- /dev/null +++ b/src/main/kotlin/app/revanced/patches/youtube/utils/fix/client/fingerprints/CreatePlaybackSpeedMenuItemFingerprint.kt @@ -0,0 +1,34 @@ +package app.revanced.patches.youtube.utils.fix.client.fingerprints + +import app.revanced.patcher.extensions.or +import app.revanced.patcher.fingerprint.MethodFingerprint +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal object CreatePlaybackSpeedMenuItemFingerprint : MethodFingerprint( + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + returnType = "V", + opcodes = listOf( + Opcode.IGET_OBJECT, // First instruction of the method + Opcode.IGET_OBJECT, + Opcode.IGET_OBJECT, + Opcode.CONST_4, + Opcode.IF_EQZ, + Opcode.INVOKE_INTERFACE, + null // MOVE_RESULT or MOVE_RESULT_OBJECT, Return value controls the creation of the playback speed menu item. + ), + // 19.01 and earlier is missing the second parameter. + // Since this fingerprint is somewhat weak, work around by checking for both method parameter signatures. + customFingerprint = custom@{ methodDef, _ -> + // 19.01 and earlier parameters are: "[L" + // 19.02+ parameters are "[L", "F" + val parameterTypes = methodDef.parameterTypes + val firstParameter = parameterTypes.firstOrNull() + + if (firstParameter == null || !firstParameter.startsWith("[L")) { + return@custom false + } + + parameterTypes.size == 1 || (parameterTypes.size == 2 && parameterTypes[1] == "F") + }, +) \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patches/youtube/utils/fix/client/fingerprints/PlayerGestureConfigSyntheticFingerprint.kt b/src/main/kotlin/app/revanced/patches/youtube/utils/fix/client/fingerprints/PlayerGestureConfigSyntheticFingerprint.kt index c99f02c43..11a135a82 100644 --- a/src/main/kotlin/app/revanced/patches/youtube/utils/fix/client/fingerprints/PlayerGestureConfigSyntheticFingerprint.kt +++ b/src/main/kotlin/app/revanced/patches/youtube/utils/fix/client/fingerprints/PlayerGestureConfigSyntheticFingerprint.kt @@ -1,9 +1,8 @@ package app.revanced.patches.youtube.utils.fix.client.fingerprints import app.revanced.patcher.extensions.or -import app.revanced.patcher.fingerprint.MethodFingerprint import app.revanced.patcher.fingerprint.annotation.FuzzyPatternScanMethod -import app.revanced.patches.youtube.utils.fix.client.fingerprints.PlayerGestureConfigSyntheticFingerprint.indexOfDownAndOutAllowedInstruction +import app.revanced.patcher.fingerprint.MethodFingerprint import app.revanced.util.getReference import app.revanced.util.indexOfFirstInstruction import com.android.tools.smali.dexlib2.AccessFlags @@ -12,8 +11,7 @@ import com.android.tools.smali.dexlib2.iface.Method import com.android.tools.smali.dexlib2.iface.reference.MethodReference /** - * Annotation [FuzzyPatternScanMethod] is required to maintain compatibility with YouTube v18.29.38 ~ v18.32.39. - * + * [FuzzyPatternScanMethod] was added to maintain compatibility for YouTube v18.29.38 to v18.32.39. * TODO: Remove this annotation if support for YouTube v18.29.38 to v18.32.39 is dropped. */ @FuzzyPatternScanMethod(5) @@ -30,28 +28,28 @@ internal object PlayerGestureConfigSyntheticFingerprint : MethodFingerprint( Opcode.IGET_OBJECT, Opcode.INVOKE_INTERFACE, Opcode.MOVE_RESULT_OBJECT, - Opcode.INVOKE_VIRTUAL, // playerGestureConfig.downAndOutLandscapeAllowed + Opcode.INVOKE_VIRTUAL, // playerGestureConfig.downAndOutLandscapeAllowed. Opcode.MOVE_RESULT, Opcode.CHECK_CAST, Opcode.IPUT_BOOLEAN, Opcode.INVOKE_INTERFACE, Opcode.MOVE_RESULT_OBJECT, - Opcode.INVOKE_VIRTUAL, // playerGestureConfig.downAndOutPortraitAllowed + Opcode.INVOKE_VIRTUAL, // playerGestureConfig.downAndOutPortraitAllowed. Opcode.MOVE_RESULT, Opcode.IPUT_BOOLEAN, Opcode.RETURN_VOID, ), customFingerprint = { methodDef, classDef -> - // This method is always called "a" because this kind of class always has a single method. - methodDef.name == "a" && classDef.methods.count() == 2 && - indexOfDownAndOutAllowedInstruction(methodDef) >= 0 - } -) { - fun indexOfDownAndOutAllowedInstruction(methodDef: Method) = - methodDef.indexOfFirstInstruction { - val reference = getReference() - reference?.definingClass == "Lcom/google/android/libraries/youtube/innertube/model/media/PlayerConfigModel;" && + fun indexOfDownAndOutAllowedInstruction(methodDef: Method) = + methodDef.indexOfFirstInstruction { + val reference = getReference() + reference?.definingClass == "Lcom/google/android/libraries/youtube/innertube/model/media/PlayerConfigModel;" && reference.parameterTypes.isEmpty() && reference.returnType == "Z" - } -} \ No newline at end of file + } + + // This method is always called "a" because this kind of class always has a single method. + methodDef.name == "a" && classDef.methods.count() == 2 && + indexOfDownAndOutAllowedInstruction(methodDef) >= 0 + }, +) \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patches/youtube/utils/flyoutmenu/FlyoutMenuHookPatch.kt b/src/main/kotlin/app/revanced/patches/youtube/utils/flyoutmenu/FlyoutMenuHookPatch.kt index de312377a..56270f123 100644 --- a/src/main/kotlin/app/revanced/patches/youtube/utils/flyoutmenu/FlyoutMenuHookPatch.kt +++ b/src/main/kotlin/app/revanced/patches/youtube/utils/flyoutmenu/FlyoutMenuHookPatch.kt @@ -2,9 +2,8 @@ package app.revanced.patches.youtube.utils.flyoutmenu import app.revanced.patcher.data.BytecodeContext import app.revanced.patcher.patch.BytecodePatch -import app.revanced.patcher.patch.PatchException import app.revanced.patcher.patch.annotation.Patch -import app.revanced.patches.youtube.utils.flyoutmenu.fingerprints.PlaybackRateBottomSheetClassFingerprint +import app.revanced.patches.youtube.utils.fingerprints.PlaybackRateBottomSheetBuilderFingerprint import app.revanced.patches.youtube.utils.flyoutmenu.fingerprints.VideoQualityBottomSheetClassFingerprint import app.revanced.patches.youtube.utils.integrations.Constants.INTEGRATIONS_PATH import app.revanced.patches.youtube.utils.resourceid.SharedResourceIdPatch @@ -17,7 +16,7 @@ import app.revanced.util.resultOrThrow ) object FlyoutMenuHookPatch : BytecodePatch( setOf( - PlaybackRateBottomSheetClassFingerprint, + PlaybackRateBottomSheetBuilderFingerprint, VideoQualityBottomSheetClassFingerprint ) ) { @@ -30,17 +29,12 @@ object FlyoutMenuHookPatch : BytecodePatch( INTEGRATIONS_VIDEO_UTILS_CLASS_DESCRIPTOR )!!.mutableClass - PlaybackRateBottomSheetClassFingerprint.resultOrThrow().let { + PlaybackRateBottomSheetBuilderFingerprint.resultOrThrow().let { it.mutableMethod.apply { - val playbackRateBottomSheetBuilderMethodName = - it.mutableClass.methods.find { method -> method.parameters.isEmpty() && method.returnType == "V" } - ?.name - ?: throw PatchException("Could not find PlaybackRateBottomSheetBuilderMethod") - val smaliInstructions = """ if-eqz v0, :ignore - invoke-virtual {v0}, $definingClass->$playbackRateBottomSheetBuilderMethodName()V + invoke-virtual {v0}, $definingClass->$name()V :ignore return-void """ diff --git a/src/main/kotlin/app/revanced/patches/youtube/utils/flyoutmenu/fingerprints/PlaybackRateBottomSheetClassFingerprint.kt b/src/main/kotlin/app/revanced/patches/youtube/utils/flyoutmenu/fingerprints/PlaybackRateBottomSheetClassFingerprint.kt deleted file mode 100644 index 0ee51db6c..000000000 --- a/src/main/kotlin/app/revanced/patches/youtube/utils/flyoutmenu/fingerprints/PlaybackRateBottomSheetClassFingerprint.kt +++ /dev/null @@ -1,12 +0,0 @@ -package app.revanced.patches.youtube.utils.flyoutmenu.fingerprints - -import app.revanced.patcher.extensions.or -import app.revanced.patcher.fingerprint.MethodFingerprint -import com.android.tools.smali.dexlib2.AccessFlags - -internal object PlaybackRateBottomSheetClassFingerprint : MethodFingerprint( - returnType = "V", - accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, - parameters = listOf("[L", "I"), - strings = listOf("menu_item_playback_speed") -) \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patches/youtube/utils/resourceid/SharedResourceIdPatch.kt b/src/main/kotlin/app/revanced/patches/youtube/utils/resourceid/SharedResourceIdPatch.kt index 41324c5c5..f7b9d4d81 100644 --- a/src/main/kotlin/app/revanced/patches/youtube/utils/resourceid/SharedResourceIdPatch.kt +++ b/src/main/kotlin/app/revanced/patches/youtube/utils/resourceid/SharedResourceIdPatch.kt @@ -95,6 +95,7 @@ object SharedResourceIdPatch : ResourcePatch() { var TotalTime = -1L var TouchArea = -1L var VideoQualityBottomSheet = -1L + var VarispeedUnavailableTitle = -1L var VideoQualityUnavailableAnnouncement = -1L var VoiceSearch = -1L var YouTubeControlsOverlaySubtitleButton = -1L @@ -187,6 +188,7 @@ object SharedResourceIdPatch : ResourcePatch() { TotalTime = getId(STRING, "total_time") TouchArea = getId(ID, "touch_area") VideoQualityBottomSheet = getId(LAYOUT, "video_quality_bottom_sheet_list_fragment_title") + VarispeedUnavailableTitle = getId(STRING, "varispeed_unavailable_title") VideoQualityUnavailableAnnouncement = getId(STRING, "video_quality_unavailable_announcement") VoiceSearch = getId(ID, "voice_search") diff --git a/src/main/kotlin/app/revanced/util/BytecodeUtils.kt b/src/main/kotlin/app/revanced/util/BytecodeUtils.kt index 3a9fae043..7b8866ce9 100644 --- a/src/main/kotlin/app/revanced/util/BytecodeUtils.kt +++ b/src/main/kotlin/app/revanced/util/BytecodeUtils.kt @@ -195,6 +195,73 @@ fun BytecodeContext.literalInstructionHook( } } +/** + * Get the index of the first instruction with the literal value or throw a [PatchException]. + * + * @throws [PatchException] if the literal index not found. + */ +fun Method.indexOfWideLiteralInstructionOrThrow(literal: Long): Int { + val index = getWideLiteralInstructionIndex(literal) + if (index < 0) { + val value = + if (literal >= 2130706432) // 0x7f000000, general resource id + String.format("%#X", literal).lowercase() + else + literal.toString() + + throw PatchException("Found literal value for: '$value' but method does not contain the id: $this") + } + + return index +} + +/** + * Get the index of the first [Instruction] that matches the predicate, starting from [startIndex]. + * + * @param startIndex Optional starting index to start searching from. + * @return -1 if the instruction is not found. + * @see indexOfFirstInstructionOrThrow + */ +fun Method.indexOfFirstInstruction(startIndex: Int = 0, predicate: Instruction.() -> Boolean): Int { + val index = this.implementation!!.instructions.drop(startIndex).indexOfFirst(predicate) + + return if (index >= 0) { + startIndex + index + } else { + -1 + } +} + +/** + * Get the index of the first [Instruction] that matches the predicate, starting from [startIndex]. + * + * @return the index of the instruction + * @throws PatchException + * @see indexOfFirstInstruction + */ +fun Method.indexOfFirstInstructionOrThrow(startIndex: Int = 0, predicate: Instruction.() -> Boolean): Int { + val index = indexOfFirstInstruction(startIndex, predicate) + if (index < 0) { + throw PatchException("Could not find instruction index") + } + return index +} + +/** + * @return The list of indices of the opcode in reverse order. + */ +fun Method.findOpcodeIndicesReversed(opcode: Opcode): List { + val indexes = implementation!!.instructions + .withIndex() + .filter { (_, instruction) -> instruction.opcode == opcode } + .map { (index, _) -> index } + .reversed() + + if (indexes.isEmpty()) throw PatchException("No ${opcode.name} instructions found in: $this") + + return indexes +} + /** * Find the index of the first wide literal instruction with the given value. * @@ -206,8 +273,6 @@ fun Method.getWideLiteralInstructionIndex(literal: Long) = implementation?.let { } } ?: -1 -fun Method.getEmptyStringInstructionIndex() = getStringInstructionIndex("") - fun Method.getStringInstructionIndex(value: String) = implementation?.let { it.instructions.indexOfFirst { instruction -> instruction.opcode == Opcode.CONST_STRING diff --git a/src/main/resources/youtube/settings/host/values/strings.xml b/src/main/resources/youtube/settings/host/values/strings.xml index 733124489..d8d12f66f 100644 --- a/src/main/resources/youtube/settings/host/values/strings.xml +++ b/src/main/resources/youtube/settings/host/values/strings.xml @@ -1496,8 +1496,7 @@ Limitation: Feed videos will play for less than 1 minute before encountering pla Side effects include: • No HDR video. -• Playback speed menu is missing. -• Higher video qualities may be missing. +• Higher video qualities may not be available. • Watch history does not work with a brand account. • Live streams cannot play as audio-only. • Live streams not available on Android 8.0." @@ -1506,14 +1505,14 @@ Side effects include: Side effects include: • No HDR video. -• Audio track menu and playback speed menu are missing. +• Audio track menu is missing. • Captions may not be available." Android TV "Spoof client to Android TV (YouTube TV). Side effects include: • No HDR video. -• Audio track menu and playback speed menu are missing. +• Audio track menu is missing. • Captions may not be available. • Some live streams are not supported for playback." Android VR