diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/AdsFilter.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/AdsFilter.java index cb262e016..40f554e23 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/AdsFilter.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/AdsFilter.java @@ -236,6 +236,17 @@ public final class AdsFilter extends Filter { return Settings.HIDE_GENERAL_ADS.get(); } + /** + * Injection point. + * 21.34+ the player overlay timely shelf handler no longer gets a dedicated method, + * so instead the result of the overlay type `String.equals` comparison is passed in. + * + * @param original Whether the overlay is the timely shelf (already being shown when not hiding ads). + */ + public static boolean allowAds(boolean original) { + return Settings.HIDE_GENERAL_ADS.get() ? false : original; + } + /** * Injection point. */ diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/misc/spoof/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/shared/misc/spoof/Fingerprints.kt index 8fb0bf2f4..fc3d5189c 100644 --- a/patches/src/main/kotlin/app/revanced/patches/shared/misc/spoof/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/shared/misc/spoof/Fingerprints.kt @@ -31,14 +31,11 @@ internal val BytecodePatchContext.buildInitPlaybackRequestMethodMatch by composi } internal val BytecodePatchContext.buildPlayerRequestURIMethodMatch by composingFirstMethod("key", "asig") { - returnType("Ljava/lang/String;") + returnType($$"Landroid/net/Uri$Builder;") opcodes( - Opcode.INVOKE_VIRTUAL, // Register holds player request URI. + Opcode.INVOKE_VIRTUAL, // The last Uri$Builder mutating call. Register holds the player request Uri$Builder. Opcode.MOVE_RESULT_OBJECT, - Opcode.IPUT_OBJECT, - Opcode.IGET_OBJECT, - Opcode.MONITOR_EXIT, - Opcode.RETURN_OBJECT, + Opcode.RETURN_OBJECT, // Returns the built player request Uri$Builder. ) } @@ -154,7 +151,7 @@ internal val BytecodePatchContext.nerdsStatsVideoFormatBuilderMethod by gettingF returnType("Ljava/lang/String;") parameterTypes("L") instructions( - "codecs=\""(), + ", codecs="(), ) } diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/misc/spoof/SpoofVideoStreamsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/misc/spoof/SpoofVideoStreamsPatch.kt index 1c918b810..52a22014d 100644 --- a/patches/src/main/kotlin/app/revanced/patches/shared/misc/spoof/SpoofVideoStreamsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/shared/misc/spoof/SpoofVideoStreamsPatch.kt @@ -82,14 +82,19 @@ internal fun spoofVideoStreamsPatch( // region Block /get_watch requests to fall back to /player requests. buildPlayerRequestURIMethodMatch.method.apply { - val invokeToStringIndex = buildPlayerRequestURIMethodMatch[0] - val uriRegister = getInstruction(invokeToStringIndex).registerC + val returnIndex = buildPlayerRequestURIMethodMatch[-1] + val builderRegister = getInstruction(returnIndex).registerA + val freeRegister = findFreeRegister(returnIndex, builderRegister) addInstructions( - invokeToStringIndex, - """ - invoke-static { v$uriRegister }, $EXTENSION_CLASS_DESCRIPTOR->blockGetWatchRequest(Landroid/net/Uri;)Landroid/net/Uri; - move-result-object v$uriRegister + returnIndex, + $$""" + invoke-virtual { v$builderRegister }, Landroid/net/Uri$Builder;->build()Landroid/net/Uri; + move-result-object v$freeRegister + invoke-static { v$freeRegister }, $$EXTENSION_CLASS_DESCRIPTOR->blockGetWatchRequest(Landroid/net/Uri;)Landroid/net/Uri; + move-result-object v$freeRegister + invoke-virtual { v$freeRegister }, Landroid/net/Uri;->buildUpon()Landroid/net/Uri$Builder; + move-result-object v$builderRegister """, ) } diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/ad/general/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/ad/general/Fingerprints.kt index 6258569ad..51a846e54 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/ad/general/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/ad/general/Fingerprints.kt @@ -71,3 +71,27 @@ internal val BytecodePatchContext.playerOverlayTimelyShelfMethod by gettingFirst ) } +/** + * 21.34+ removed the [String]"innertube_cue_range", "Null id" and "Null onExitActions" strings + * from the player overlay timely shelf handler, and the handler moved into a synthetic lambda + * (`a(Ljava/lang/Object;)V`) that dispatches on the overlay type via a `String.equals` comparison. + * Only [String]"player_overlay_timely_shelf" remains, so match on it plus the following + * `String.equals`/`move-result` used to check if the overlay is the timely shelf. + */ +internal val BytecodePatchContext.playerOverlayTimelyShelfMethod21_34 by composingFirstMethod { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returnType("V") + parameterTypes("Ljava/lang/Object;") + instructions( + "player_overlay_timely_shelf"(), + afterAtMost( + 5, + allOf( + Opcode.INVOKE_VIRTUAL(), + method { name == "equals" && definingClass == "Ljava/lang/String;" }, + ), + ), + after(Opcode.MOVE_RESULT()), + ) +} + diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/ad/general/HideAdsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/ad/general/HideAdsPatch.kt index c9239da9b..8f0a7cea0 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/ad/general/HideAdsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/ad/general/HideAdsPatch.kt @@ -24,6 +24,7 @@ import app.revanced.patches.youtube.misc.engagement.engagementPanelHookPatch import app.revanced.patches.youtube.misc.fix.backtoexitgesture.fixBackToExitGesturePatch import app.revanced.patches.youtube.misc.litho.filter.lithoFilterPatch import app.revanced.patches.youtube.misc.playservice.is_20_14_or_greater +import app.revanced.patches.youtube.misc.playservice.is_21_34_or_greater import app.revanced.patches.youtube.misc.playservice.versionCheckPatch import app.revanced.patches.youtube.misc.settings.PreferenceScreen import app.revanced.patches.youtube.misc.settings.settingsPatch @@ -34,6 +35,7 @@ import app.revanced.util.indexOfFirstInstructionReversedOrThrow import app.revanced.util.injectHideViewCall import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction private const val EXTENSION_CLASS_DESCRIPTOR = @@ -94,7 +96,8 @@ val hideAdsPatch = bytecodePatch( "20.26.46", "20.31.42", "20.37.48", - "20.40.45" + "20.40.45", + "21.34.248" ), ) @@ -154,17 +157,41 @@ val hideAdsPatch = bytecodePatch( // Hide player overlay view. This can be hidden with a regular litho filter // but an empty space remains. if (is_20_14_or_greater) { - playerOverlayTimelyShelfMethod.addInstructionsWithLabels( - 0, - """ - invoke-static {}, ${EXTENSION_CLASS_DESCRIPTOR}->hideAds()Z - move-result v0 - if-eqz v0, :show - return-void - :show - nop - """ - ) + if (is_21_34_or_greater) { + // 21.34+ the overlay handler moved into a synthetic lambda `a(Object)V` + // that dispatches on overlay type. Hook right after the `String.equals` / + // `move-result` that determines the overlay is the timely shelf so that + // only the timely shelf is hidden and not every player overlay. + playerOverlayTimelyShelfMethod21_34.let { match -> + val insertIndex = match[-1] + val equalsResultRegister = + match.method.getInstruction(insertIndex).registerA + + match.method.addInstructionsWithLabels( + insertIndex + 1, + """ + invoke-static { v$equalsResultRegister }, ${EXTENSION_CLASS_DESCRIPTOR}->allowAds(Z)Z + move-result v$equalsResultRegister + if-nez v$equalsResultRegister, :show + return-void + :show + nop + """, + ) + } + } else { + playerOverlayTimelyShelfMethod.addInstructionsWithLabels( + 0, + """ + invoke-static {}, ${EXTENSION_CLASS_DESCRIPTOR}->hideAds()Z + move-result v0 + if-eqz v0, :show + return-void + :show + nop + """ + ) + } } diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/endscreencards/HideEndScreenCardsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/endscreencards/HideEndScreenCardsPatch.kt index e6f36cd84..507e18aa6 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/endscreencards/HideEndScreenCardsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/endscreencards/HideEndScreenCardsPatch.kt @@ -13,6 +13,7 @@ import app.revanced.patches.shared.misc.mapping.resourceMappingPatch import app.revanced.patches.shared.misc.settings.preference.SwitchPreference import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch import app.revanced.patches.youtube.misc.playservice.is_19_43_or_greater +import app.revanced.patches.youtube.misc.playservice.is_21_34_or_greater import app.revanced.patches.youtube.misc.playservice.versionCheckPatch import app.revanced.patches.youtube.misc.settings.PreferenceScreen import app.revanced.patches.youtube.misc.settings.settingsPatch @@ -68,7 +69,8 @@ val hideEndScreenCardsPatch = bytecodePatch( "20.26.46", "20.31.42", "20.37.48", - "20.40.45" + "20.40.45", + "21.34.248" ), ) @@ -90,7 +92,11 @@ val hideEndScreenCardsPatch = bytecodePatch( } } - if (is_19_43_or_greater) { + // 21.34+ the end screen cards are no longer shown through a dedicated 5-method + // container class that the legacy showEndscreenCardsMethod fingerprint can locate. + // Instead each card view (circle/icon/video) is created and hidden individually by the + // three layout fingerprints hooked above, which cover hiding the whole card on 21.34. + if (is_19_43_or_greater && !is_21_34_or_greater) { showEndscreenCardsMethod.addInstructionsWithLabels( 0, """ diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/miniplayer/MiniplayerPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/miniplayer/MiniplayerPatch.kt index 0ef561d71..2d4782447 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/miniplayer/MiniplayerPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/miniplayer/MiniplayerPatch.kt @@ -92,8 +92,15 @@ val miniplayerPatch = bytecodePatch( val preferences = mutableSetOf() preferences += - if (is_20_37_or_greater) { + if (is_20_37_or_greater && !is_21_34_or_greater) { ListPreference("revanced_miniplayer_type") + } else if (is_21_34_or_greater) { + // YouTube 21.29+ removed all modern miniplayers except "Modern 4", + // so the type picker no longer has any effect. + NonInteractivePreference( + key = "revanced_miniplayer_type", + summaryKey = "revanced_miniplayer_type_not_available_summary", + ) } else if (is_20_03_or_greater) { ListPreference( key = "revanced_miniplayer_type", @@ -153,7 +160,9 @@ val miniplayerPatch = bytecodePatch( ) } - preferences += TextPreference("revanced_miniplayer_opacity", inputType = InputType.NUMBER) + if (!is_21_34_or_greater) { + preferences += TextPreference("revanced_miniplayer_opacity", inputType = InputType.NUMBER) + } PreferenceScreen.PLAYER.addPreferences( PreferenceScreenPreference( @@ -265,101 +274,113 @@ val miniplayerPatch = bytecodePatch( // region Enable modern miniplayer. - miniplayerModernConstructorMethod.classDef.methods.forEach { - it.apply { - if (AccessFlags.CONSTRUCTOR.isSet(accessFlags)) { - val iPutIndex = indexOfFirstInstructionOrThrow { - this.opcode == Opcode.IPUT && this.getReference()?.type == "I" - } + // Select which modern miniplayer type is used. + // YouTube 21.29+ removed all modern miniplayers except "Modern 4", so there is + // nothing to choose between and this override is dead code. + if (!is_21_34_or_greater) { + miniplayerModernConstructorMethod.classDef.methods.forEach { + it.apply { + if (AccessFlags.CONSTRUCTOR.isSet(accessFlags)) { + val iPutIndex = indexOfFirstInstructionOrThrow { + this.opcode == Opcode.IPUT && this.getReference()?.type == "I" + } - insertModernMiniplayerTypeOverride(iPutIndex) - } else { - findReturnIndicesReversed().forEach { index -> - insertModernMiniplayerOverride( - index - ) + insertModernMiniplayerTypeOverride(iPutIndex) + } else { + findReturnIndicesReversed().forEach { index -> + insertModernMiniplayerOverride( + index + ) + } } } } } - if (is_19_23_or_greater) { - miniplayerModernConstructorMethod.insertMiniplayerFeatureFlagBooleanOverride( - MINIPLAYER_DRAG_DROP_FEATURE_KEY, - "getMiniplayerDragAndDrop", - ) - } - - if (is_19_25_or_greater) { - miniplayerModernConstructorMethod.insertMiniplayerFeatureFlagBooleanOverride( - MINIPLAYER_MODERN_FEATURE_LEGACY_KEY, - "getModernMiniplayerOverride", - ) - - miniplayerModernConstructorMethod.insertMiniplayerFeatureFlagBooleanOverride( - MINIPLAYER_MODERN_FEATURE_KEY, - "getModernFeatureFlagsActiveOverride", - ) - - miniplayerModernConstructorMethod.insertMiniplayerFeatureFlagBooleanOverride( - MINIPLAYER_DOUBLE_TAP_FEATURE_KEY, - "getMiniplayerDoubleTapAction", - ) - } - - if (is_19_26_or_greater) { - miniplayerModernConstructorMethod.apply { - val literalIndex = indexOfFirstLiteralInstructionOrThrow( - MINIPLAYER_INITIAL_SIZE_FEATURE_KEY, - ) - val targetIndex = indexOfFirstInstructionOrThrow(literalIndex, Opcode.LONG_TO_INT) - val register = getInstruction(targetIndex).registerA - - addInstructions( - targetIndex + 1, - """ - invoke-static { v$register }, $EXTENSION_CLASS_DESCRIPTOR->getMiniplayerDefaultSize(I)I - move-result v$register - """, + // Modern miniplayer feature-flag overrides. + // YouTube 21.29+ removed all modern miniplayers except "Modern 4" and moved/removed the + // feature flags for drag & drop, double-tap, rounded corners, horizontal drag and animated + // expand. The default/minimum size flags were removed in 21.32+. The fingerprints below + // therefore no longer resolve on 21.34+, so these overrides only apply to older versions. + if (!is_21_34_or_greater) { + if (is_19_23_or_greater) { + miniplayerModernConstructorMethod.insertMiniplayerFeatureFlagBooleanOverride( + MINIPLAYER_DRAG_DROP_FEATURE_KEY, + "getMiniplayerDragAndDrop", ) } - // Override a minimum size constant. - miniplayerMinimumSizeMethodMatch.let { - it.method.apply { - val index = it[1] - val register = getInstruction(index).registerA + if (is_19_25_or_greater) { + miniplayerModernConstructorMethod.insertMiniplayerFeatureFlagBooleanOverride( + MINIPLAYER_MODERN_FEATURE_LEGACY_KEY, + "getModernMiniplayerOverride", + ) - // Smaller sizes can be used, but the miniplayer will always start in size 170 if set any smaller. - // The 170 initial limit probably could be patched to allow even smaller initial sizes, - // but 170 is already half the horizontal space and smaller does not seem useful. - replaceInstruction(index, "const/16 v$register, 170") + miniplayerModernConstructorMethod.insertMiniplayerFeatureFlagBooleanOverride( + MINIPLAYER_MODERN_FEATURE_KEY, + "getModernFeatureFlagsActiveOverride", + ) + + miniplayerModernConstructorMethod.insertMiniplayerFeatureFlagBooleanOverride( + MINIPLAYER_DOUBLE_TAP_FEATURE_KEY, + "getMiniplayerDoubleTapAction", + ) + } + + if (is_19_26_or_greater) { + miniplayerModernConstructorMethod.apply { + val literalIndex = indexOfFirstLiteralInstructionOrThrow( + MINIPLAYER_INITIAL_SIZE_FEATURE_KEY, + ) + val targetIndex = indexOfFirstInstructionOrThrow(literalIndex, Opcode.LONG_TO_INT) + val register = getInstruction(targetIndex).registerA + + addInstructions( + targetIndex + 1, + """ + invoke-static { v$register }, $EXTENSION_CLASS_DESCRIPTOR->getMiniplayerDefaultSize(I)I + move-result v$register + """, + ) + } + + // Override a minimum size constant. + miniplayerMinimumSizeMethodMatch.let { + it.method.apply { + val index = it[1] + val register = getInstruction(index).registerA + + // Smaller sizes can be used, but the miniplayer will always start in size 170 if set any smaller. + // The 170 initial limit probably could be patched to allow even smaller initial sizes, + // but 170 is already half the horizontal space and smaller does not seem useful. + replaceInstruction(index, "const/16 v$register, 170") + } } } - } - if (is_19_36_or_greater) { - miniplayerModernConstructorMethod.insertMiniplayerFeatureFlagBooleanOverride( - MINIPLAYER_ROUNDED_CORNERS_FEATURE_KEY, - "getRoundedCorners", - ) - } + if (is_19_36_or_greater) { + miniplayerModernConstructorMethod.insertMiniplayerFeatureFlagBooleanOverride( + MINIPLAYER_ROUNDED_CORNERS_FEATURE_KEY, + "getRoundedCorners", + ) + } - if (is_19_43_or_greater) { - miniplayerOnCloseHandlerMethod.insertMiniplayerFeatureFlagBooleanOverride( - MINIPLAYER_DISABLED_FEATURE_KEY, - "getMiniplayerOnCloseHandler", - ) + if (is_19_43_or_greater) { + miniplayerOnCloseHandlerMethod.insertMiniplayerFeatureFlagBooleanOverride( + MINIPLAYER_DISABLED_FEATURE_KEY, + "getMiniplayerOnCloseHandler", + ) - miniplayerModernConstructorMethod.insertMiniplayerFeatureFlagBooleanOverride( - MINIPLAYER_HORIZONTAL_DRAG_FEATURE_KEY, - "getHorizontalDrag", - ) + miniplayerModernConstructorMethod.insertMiniplayerFeatureFlagBooleanOverride( + MINIPLAYER_HORIZONTAL_DRAG_FEATURE_KEY, + "getHorizontalDrag", + ) - miniplayerModernConstructorMethod.insertMiniplayerFeatureFlagBooleanOverride( - MINIPLAYER_ANIMATED_EXPAND_FEATURE_KEY, - "getMaximizeAnimation", - ) + miniplayerModernConstructorMethod.insertMiniplayerFeatureFlagBooleanOverride( + MINIPLAYER_ANIMATED_EXPAND_FEATURE_KEY, + "getMaximizeAnimation", + ) + } } // endregion @@ -391,7 +412,8 @@ val miniplayerPatch = bytecodePatch( // region fix minimal miniplayer using the wrong pause/play bold icons. - if (is_20_31_or_greater) { + // 21.34 does not have the yt_fill_pause_* drawables this fingerprint anchors on. + if (is_20_31_or_greater && !is_21_34_or_greater) { miniplayerSetIconsMethod.apply { findInstructionIndicesReversedOrThrow { val reference = getReference() @@ -421,7 +443,6 @@ val miniplayerPatch = bytecodePatch( ClassDef::miniplayerModernActionButtonMethodMatch to "hideMiniplayerActionButton", ClassDef::miniplayerModernRewindButtonMethodMatch to "hideMiniplayerRewindForward", ClassDef::miniplayerModernForwardButtonMethodMatch to "hideMiniplayerRewindForward", - ClassDef::miniplayerModernOverlayViewMethodMatch to "adjustMiniplayerOpacity", ).forEach { (matching, methodName) -> val match = matching.get(miniplayerModernViewParentMethod.immutableClassDef) @@ -436,6 +457,23 @@ val miniplayerPatch = bytecodePatch( } } + // Adjust the overlay opacity. The modern overlay was restructured in 21.29+ alongside the + // removal of the default/minimum size flags, so this hook is only applied on older versions. + if (!is_21_34_or_greater) { + val match = ClassDef::miniplayerModernOverlayViewMethodMatch + .get(miniplayerModernViewParentMethod.immutableClassDef) + + match.method.apply { + val index = match[-1] + val register = getInstruction(index).registerA + + addInstruction( + index + 1, + "invoke-static { v$register }, $EXTENSION_CLASS_DESCRIPTOR->adjustMiniplayerOpacity(Landroid/view/View;)V", + ) + } + } + miniplayerModernViewParentMethod.immutableClassDef.getMiniplayerModernAddViewListenerMethod() .addInstruction( 0, diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/shortsautoplay/ShortsAutoplayPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/shortsautoplay/ShortsAutoplayPatch.kt index 63fa21a21..90380a33a 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/shortsautoplay/ShortsAutoplayPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/shortsautoplay/ShortsAutoplayPatch.kt @@ -15,6 +15,7 @@ import app.revanced.patches.shared.misc.settings.preference.SwitchPreference import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch import app.revanced.patches.youtube.misc.playservice.is_19_34_or_greater import app.revanced.patches.youtube.misc.playservice.is_20_09_or_greater +import app.revanced.patches.youtube.misc.playservice.is_21_34_or_greater import app.revanced.patches.youtube.misc.playservice.versionCheckPatch import app.revanced.patches.youtube.misc.settings.PreferenceScreen import app.revanced.patches.youtube.misc.settings.settingsPatch @@ -93,103 +94,109 @@ val shortsAutoplayPatch = bytecodePatch( ) } - val reelPlaybackRepeatMethod = - reelPlaybackRepeatParentMethod.immutableClassDef.getReelPlaybackRepeatMethod() - - reelPlaybackRepeatMethod.apply { - // The behavior enums are looked up from an ordinal value to an enum type. - findInstructionIndicesReversedOrThrow { - val reference = getReference() - reference?.definingClass == reelEnumClass && - reference.parameterTypes.firstOrNull() == "I" && - reference.returnType == reelEnumClass - }.forEach { index -> - val register = getInstruction(index + 1).registerA - - addInstructions( - index + 2, - """ - invoke-static {v$register}, $EXTENSION_CLASS_DESCRIPTOR->changeShortsRepeatBehavior(Ljava/lang/Enum;)Ljava/lang/Enum; - move-result-object v$register - """, - ) - } - } - - // As of YouTube 20.09, Google has removed the code for 'Autoplay' and 'Pause' from this method. - // Manually restore the removed 'Autoplay' code. - if (is_20_09_or_greater) { - // Variable names are only a rough guess of what these methods do. - val userActionMethodReference = - reelPlaybackMethodMatch.method.getInstruction(reelPlaybackMethodMatch[1]).methodReference!! - val reelSequenceControllerMethodReference = - reelPlaybackMethodMatch.method.getInstruction(reelPlaybackMethodMatch[2]).methodReference!! + // YouTube 21.29+ replaced the diagnostics string and reworked the reel playback repeat + // method (its signature changed from "V(L)" to "bL(JLbetu;J)V"), so the parent fingerprint + // below no longer resolves on 21.34+. The autoplay restore logic thus only applies to + // versions below 21.34. + if (!is_21_34_or_greater) { + val reelPlaybackRepeatMethod = + reelPlaybackRepeatParentMethod.immutableClassDef.getReelPlaybackRepeatMethod() reelPlaybackRepeatMethod.apply { - // Find the first call modified by extension code above. - val extensionReturnResultIndex = indexOfFirstInstructionOrThrow { - opcode == Opcode.INVOKE_STATIC && - getReference()?.definingClass == EXTENSION_CLASS_DESCRIPTOR - } + 1 - val enumRegister = - getInstruction(extensionReturnResultIndex).registerA - val getReelSequenceControllerIndex = indexOfFirstInstructionOrThrow { - val reference = getReference() - opcode == Opcode.IGET_OBJECT && - reference?.definingClass == definingClass && - reference.type == reelSequenceControllerMethodReference.definingClass - } - val getReelSequenceControllerReference = - getInstruction(getReelSequenceControllerIndex).reference + // The behavior enums are looked up from an ordinal value to an enum type. + findInstructionIndicesReversedOrThrow { + val reference = getReference() + reference?.definingClass == reelEnumClass && + reference.parameterTypes.firstOrNull() == "I" && + reference.returnType == reelEnumClass + }.forEach { index -> + val register = getInstruction(index + 1).registerA - // Add a helper method to avoid finding multiple free registers. - // If enum is autoplay then method performs autoplay and returns null, - // otherwise returns the same enum. - val helperClass = definingClass - val helperName = "patch_handleAutoPlay" - val helperReturnType = "Ljava/lang/Enum;" - val helperMethod = ImmutableMethod( - helperClass, - helperName, - listOf(ImmutableMethodParameter("Ljava/lang/Enum;", null, null)), - helperReturnType, - AccessFlags.PRIVATE.value, - null, - null, - MutableMethodImplementation(7), - ).toMutable().apply { - addInstructionsWithLabels( - 0, + addInstructions( + index + 2, """ - invoke-static { p1 }, $EXTENSION_CLASS_DESCRIPTOR->isAutoPlay(Ljava/lang/Enum;)Z - move-result v0 - if-eqz v0, :ignore - new-instance v0, ${userActionMethodReference.definingClass} - const/4 v1, 0x3 - const/4 v2, 0x0 - invoke-direct { v0, v1, v2, v2 }, $userActionMethodReference - iget-object v3, p0, $getReelSequenceControllerReference - invoke-virtual { v3, v0 }, $reelSequenceControllerMethodReference - const/4 v4, 0x0 - return-object v4 - :ignore - return-object p1 + invoke-static {v$register}, $EXTENSION_CLASS_DESCRIPTOR->changeShortsRepeatBehavior(Ljava/lang/Enum;)Ljava/lang/Enum; + move-result-object v$register """, ) } - reelPlaybackRepeatMethod.classDef.methods.add(helperMethod) + } - addInstructionsWithLabels( - extensionReturnResultIndex + 1, - """ - invoke-direct { p0, v$enumRegister }, $helperClass->$helperName(Ljava/lang/Enum;)$helperReturnType - move-result-object v$enumRegister - if-nez v$enumRegister, :ignore - return-void # Autoplay was performed. - :ignore - nop - """, - ) + // As of YouTube 20.09, Google has removed the code for 'Autoplay' and 'Pause' from this method. + // Manually restore the removed 'Autoplay' code. + if (is_20_09_or_greater) { + // Variable names are only a rough guess of what these methods do. + val userActionMethodReference = + reelPlaybackMethodMatch.method.getInstruction(reelPlaybackMethodMatch[1]).methodReference!! + val reelSequenceControllerMethodReference = + reelPlaybackMethodMatch.method.getInstruction(reelPlaybackMethodMatch[2]).methodReference!! + + reelPlaybackRepeatMethod.apply { + // Find the first call modified by extension code above. + val extensionReturnResultIndex = indexOfFirstInstructionOrThrow { + opcode == Opcode.INVOKE_STATIC && + getReference()?.definingClass == EXTENSION_CLASS_DESCRIPTOR + } + 1 + val enumRegister = + getInstruction(extensionReturnResultIndex).registerA + val getReelSequenceControllerIndex = indexOfFirstInstructionOrThrow { + val reference = getReference() + opcode == Opcode.IGET_OBJECT && + reference?.definingClass == definingClass && + reference.type == reelSequenceControllerMethodReference.definingClass + } + val getReelSequenceControllerReference = + getInstruction(getReelSequenceControllerIndex).reference + + // Add a helper method to avoid finding multiple free registers. + // If enum is autoplay then method performs autoplay and returns null, + // otherwise returns the same enum. + val helperClass = definingClass + val helperName = "patch_handleAutoPlay" + val helperReturnType = "Ljava/lang/Enum;" + val helperMethod = ImmutableMethod( + helperClass, + helperName, + listOf(ImmutableMethodParameter("Ljava/lang/Enum;", null, null)), + helperReturnType, + AccessFlags.PRIVATE.value, + null, + null, + MutableMethodImplementation(7), + ).toMutable().apply { + addInstructionsWithLabels( + 0, + """ + invoke-static { p1 }, $EXTENSION_CLASS_DESCRIPTOR->isAutoPlay(Ljava/lang/Enum;)Z + move-result v0 + if-eqz v0, :ignore + new-instance v0, ${userActionMethodReference.definingClass} + const/4 v1, 0x3 + const/4 v2, 0x0 + invoke-direct { v0, v1, v2, v2 }, $userActionMethodReference + iget-object v3, p0, $getReelSequenceControllerReference + invoke-virtual { v3, v0 }, $reelSequenceControllerMethodReference + const/4 v4, 0x0 + return-object v4 + :ignore + return-object p1 + """, + ) + } + reelPlaybackRepeatMethod.classDef.methods.add(helperMethod) + + addInstructionsWithLabels( + extensionReturnResultIndex + 1, + """ + invoke-direct { p0, v$enumRegister }, $helperClass->$helperName(Ljava/lang/Enum;)$helperReturnType + move-result-object v$enumRegister + if-nez v$enumRegister, :ignore + return-void # Autoplay was performed. + :ignore + nop + """, + ) + } } } } diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/backgroundplayback/BackgroundPlaybackPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/backgroundplayback/BackgroundPlaybackPatch.kt index 6f4944eaa..47cae7b03 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/backgroundplayback/BackgroundPlaybackPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/backgroundplayback/BackgroundPlaybackPatch.kt @@ -12,6 +12,7 @@ import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch import app.revanced.patches.youtube.misc.playertype.playerTypeHookPatch import app.revanced.patches.youtube.misc.playservice.is_19_34_or_greater import app.revanced.patches.youtube.misc.playservice.is_20_29_or_greater +import app.revanced.patches.youtube.misc.playservice.is_21_34_or_greater import app.revanced.patches.youtube.misc.playservice.versionCheckPatch import app.revanced.patches.youtube.misc.settings.PreferenceScreen import app.revanced.patches.youtube.misc.settings.settingsPatch @@ -103,7 +104,10 @@ val removeBackgroundPlaybackRestrictionsPatch = bytecodePatch( kidsBackgroundPlaybackPolicyControllerMethod.returnEarly() // Fix PiP buttons not working after locking/unlocking device screen. - if (is_19_34_or_greater) { + // YouTube 21.34+ removed the flag this anchors on (the remaining case is an + // unfixable Google-side issue, see RVX #2764). Skipping it avoids the + // fingerprint being unresolved and crashing the whole patch. + if (is_19_34_or_greater && !is_21_34_or_greater) { pipInputConsumerFeatureFlagMethodMatch.let { it.method.insertLiteralOverride( it[0], diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/contexthook/ClientContextHookPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/contexthook/ClientContextHookPatch.kt index ec82e4bdb..64640d4ba 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/contexthook/ClientContextHookPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/contexthook/ClientContextHookPatch.kt @@ -15,6 +15,7 @@ import app.revanced.patcher.patch.BytecodePatchContext import app.revanced.patcher.patch.bytecodePatch import app.revanced.patcher.returnType import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch +import app.revanced.patches.youtube.misc.playservice.is_21_34_or_greater import app.revanced.util.addInstructionsAtControlFlowLabel import app.revanced.util.cloneMutableAndPreserveParameters import app.revanced.util.findInstructionIndicesReversedOrThrow @@ -73,12 +74,25 @@ val hookClientContextPatch = bytecodePatch( } } - authenticationChangeListenerMethod.apply { - val messageLiteBuilderIndex = - indexOfMessageLiteBuilderReference(this, messageLiteBuilderField.definingClass) + // The Authentication changed listener method (where the message lite builder method was + // sourced from) was removed in YouTube 21.34+. Derive the builder method from the dummy + // client context body method instead, which still resolves and builds the same body. + if (!is_21_34_or_greater) { + authenticationChangeListenerMethod.apply { + val messageLiteBuilderIndex = + indexOfMessageLiteBuilderReference(this, messageLiteBuilderField.definingClass) - messageLiteBuilderMethod = - getInstruction(messageLiteBuilderIndex).methodReference!! + messageLiteBuilderMethod = + getInstruction(messageLiteBuilderIndex).methodReference!! + } + } else { + buildDummyClientContextBodyMethodMatch.method.apply { + val messageLiteBuilderIndex = + indexOfMessageLiteBuilderReference(this, messageLiteBuilderField.definingClass) + + messageLiteBuilderMethod = + getInstruction(messageLiteBuilderIndex).methodReference!! + } } buildClientContextBodyConstructorMethod.immutableClassDef.buildClientContextBodyMethodMatch.let { diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/gms/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/gms/Fingerprints.kt index b434de2bb..966d00f54 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/gms/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/gms/Fingerprints.kt @@ -11,7 +11,6 @@ internal val BytecodePatchContext.specificNetworkErrorViewControllerMethodMatch returnType("V") parameterTypes() instructions( - ResourceType.DRAWABLE("ic_offline_no_content_upside_down"), ResourceType.STRING("offline_no_content_body_text_not_offline_eligible"), method { name == "getString" && returnType == "Ljava/lang/String;" }, after(Opcode.MOVE_RESULT_OBJECT()), @@ -20,12 +19,13 @@ internal val BytecodePatchContext.specificNetworkErrorViewControllerMethodMatch // It's not clear if this second class is ever used, and it may be dead code, // but it is the layout image/text is identical to the network error match above. +// The offline image drawable (ic_offline_no_content_upside_down) was removed in 21.34, +// so only the still-present offline_no_content string is matched. internal val BytecodePatchContext.loadingFrameLayoutControllerMethodMatch by composingFirstMethod { accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) returnType("V") - parameterTypes("L") + parameterTypes("Z") instructions( - ResourceType.DRAWABLE("ic_offline_no_content_upside_down"), ResourceType.STRING("offline_no_content_body_text_not_offline_eligible"), method { name == "getString" && returnType == "Ljava/lang/String;" }, after(Opcode.MOVE_RESULT_OBJECT()), diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/litho/filter/LithoFilterPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/litho/filter/LithoFilterPatch.kt index 772e5b6ef..609938660 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/litho/filter/LithoFilterPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/litho/filter/LithoFilterPatch.kt @@ -59,13 +59,18 @@ val lithoFilterPatch = lithoFilterPatch( } // Turn off a feature flag that enables native code of protobuf parsing (Upb protobuf). - lithoConverterBufferUpbFeatureFlagMethodMatch.let { - // 20.22 the flag is still enabled in one location, but what it does is not known. - // Disable it anyway. - it.method.insertLiteralOverride( - it[0], - false, - ) + // The flag (45419603) was removed in 21.34. In 21.34 the native UPB buffer path is the + // default (and required) path for the setProtoBuffer([B) hook, so nothing needs to be + // forced off. + if (!is_21_34_or_greater) { + lithoConverterBufferUpbFeatureFlagMethodMatch.let { + // 20.22 the flag is still enabled in one location, but what it does is not known. + // Disable it anyway. + it.method.insertLiteralOverride( + it[0], + false, + ) + } } // endregion } diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/playercontrols/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/playercontrols/Fingerprints.kt index f289bc903..64e39f740 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/playercontrols/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/playercontrols/Fingerprints.kt @@ -34,6 +34,27 @@ internal val ClassDef.motionEventMethodMatch by ClassDefComposing.composingFirst instructions(method("setTranslationY")) } +// The player controls overlay was rebuilt in 21.34 and no longer calls `setTranslationY`. +// Instead the tap-to-dismiss handler is `nwo.b(Landroid/view/MotionEvent;)V`, which triggers the +// visibility change via `nwo.U()V`. These two fingerprints resolve that new hook and keep the +// `visibilityNegatedImmediate` injection working for the newer layout. +internal val BytecodePatchContext.youtubeControlsOverlayMethod21_34 by gettingFirstImmutableMethodDeclaratively { + returnType("V") + parameterTypes("Landroid/view/MotionEvent;") + instructions( + field { definingClass == "Lnwo;" && name == "br" }, + method { definingClass == "Lnwo;" && name == "U" }, + ) +} + +internal val ClassDef.motionEventMethodMatch21_34 by ClassDefComposing.composingFirstMethod { + returnType("V") + parameterTypes("Landroid/view/MotionEvent;") + instructions( + method { definingClass == "Lnwo;" && name == "U" }, + ) +} + internal val BytecodePatchContext.playerControlsExtensionHookListenersExistMethod by gettingFirstMethodDeclaratively { name("fullscreenButtonVisibilityCallbacksExist") definingClass(EXTENSION_CLASS_DESCRIPTOR) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/playercontrols/PlayerControlsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/playercontrols/PlayerControlsPatch.kt index 3dde03331..7e32712c1 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/playercontrols/PlayerControlsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/playercontrols/PlayerControlsPatch.kt @@ -268,7 +268,13 @@ val playerControlsPatch = bytecodePatch( visibilityImmediateCallbacksExistMethod = playerControlsExtensionHookListenersExistMethod visibilityImmediateMethod = playerControlsExtensionHookMethod - youtubeControlsOverlayMethod.immutableClassDef.motionEventMethodMatch.let { + val motionEventMethodMatch = if (is_21_34_or_greater) { + youtubeControlsOverlayMethod21_34.immutableClassDef.motionEventMethodMatch21_34 + } else { + youtubeControlsOverlayMethod.immutableClassDef.motionEventMethodMatch + } + + motionEventMethodMatch.let { visibilityNegatedImmediateMethod = it.method visibilityNegatedImmediateInsertIndex = it[0] + 1 } diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/playservice/VersionCheckPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/playservice/VersionCheckPatch.kt index f2e6b41ee..b39eb5ae6 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/playservice/VersionCheckPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/playservice/VersionCheckPatch.kt @@ -127,6 +127,8 @@ var is_21_07_or_greater : Boolean by Delegates.notNull() private set var is_21_08_or_greater : Boolean by Delegates.notNull() private set +var is_21_34_or_greater: Boolean by Delegates.notNull() + private set val versionCheckPatch = resourcePatch( description = "Uses the Play Store service version to find the major/minor version of the YouTube target app.", @@ -184,5 +186,6 @@ val versionCheckPatch = resourcePatch( is_21_06_or_greater = 260705000 <= playStoreServicesVersion is_21_07_or_greater = 260805000 <= playStoreServicesVersion is_21_08_or_greater = 260905000 <= playStoreServicesVersion + is_21_34_or_greater = 263410000 <= playStoreServicesVersion } } diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/settings/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/settings/Fingerprints.kt index 646751658..321793707 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/settings/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/settings/Fingerprints.kt @@ -21,6 +21,27 @@ internal val BytecodePatchContext.setThemeMethod by gettingFirstMethodDeclarativ instructions(ResourceType.STRING("app_theme_appearance_dark")) } +/** + * YouTube 21.34+ appearance resolver `Latyq;->t()Lkll;`. + * + * Returns the theme appearance enum (whose ordinal 1 is dark), resolving between the + * `app_theme_appearance_*` prefs, the dark-mode flag and the system UI mode. Among all + * `public final ()Lkll` methods it is uniquely identified by reading `Lklm;->f` (the stored + * appearance value string): the sibling helpers `u()`/`v()` read `Lklm;->e`/the system UI mode + * instead, while `qyn` reads `Lklm;->f` but returns `V`. + */ +internal val BytecodePatchContext.setThemeMethod21_34 by gettingFirstMethodDeclaratively { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returnType("Lkll;") + parameterTypes() + instructions( + allOf( + Opcode.IGET_OBJECT(), + field { name == "f" && type == "Ljava/lang/String;" && definingClass == "Lklm;" }, + ), + ) +} + internal val BytecodePatchContext.cairoFragmentConfigMethodMatch by composingFirstMethod { accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) returnType("Z") @@ -29,3 +50,22 @@ internal val BytecodePatchContext.cairoFragmentConfigMethodMatch by composingFir afterAtMost(10, Opcode.MOVE_RESULT()), ) } + +/** + * YouTube 21.34+ Cairo settings gate `Loqy;->run()V`. + * + * The legacy `public final ()Z` feature-flag gate no longer exists. In 21.34 the Cairo settings + * screen is selected by feature flag `0x2b6c3c4` inside `Loqy;->run()V`, which chooses between the + * legacy (`xml/settings_fragment`, 0x7f180027) and Cairo (`xml/settings_fragment_cairo`, + * 0x7f180028) settings screens. The `0x7f180028` literal only appears in this method, which keeps + * it unambiguous among the other `0x2b6c3c4` references. + */ +internal val BytecodePatchContext.cairoFragmentConfigMethodMatch21_34 by composingFirstMethod { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returnType("V") + instructions( + 45532100L(), + afterAtMost(10, Opcode.MOVE_RESULT()), + afterAtMost(10, 2132279336L()), + ) +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/settings/SettingsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/settings/SettingsPatch.kt index dd3dd0176..b6fc77853 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/settings/SettingsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/settings/SettingsPatch.kt @@ -23,6 +23,7 @@ import app.revanced.patches.youtube.misc.fix.contentprovider.fixContentProviderP import app.revanced.patches.youtube.misc.fix.playbackspeed.fixPlaybackSpeedWhilePlayingPatch import app.revanced.patches.youtube.misc.playservice.is_19_34_or_greater import app.revanced.patches.youtube.misc.playservice.is_20_31_or_greater +import app.revanced.patches.youtube.misc.playservice.is_21_34_or_greater import app.revanced.patches.youtube.misc.playservice.versionCheckPatch import app.revanced.util.* import com.android.tools.smali.dexlib2.AccessFlags @@ -214,6 +215,64 @@ val settingsPatch = bytecodePatch( ) PreferenceScreen.MISC.addPreferences( + PreferenceCategory( + titleKey = "revanced_custom_title", + sorting = Sorting.UNSORTED, + tag = "app.revanced.extension.shared.settings.preference.NoTitlePreferenceCategory", + preferences = setOf( + NonInteractivePreference( + key = "revanced_custom_reset_title", + titleKey = "revanced_custom_reset_title", + summaryKey = "revanced_custom_reset_summary", + tag = "app.revanced.extension.shared.settings.preference.CustomSettingsResetPreference", + ), + SwitchPreference( + "revanced_custom_force_amoled", + ), + SwitchPreference( + "revanced_custom_pure_black_player", + ), + SwitchPreference( + "revanced_custom_disable_ambient_mode", + ), + SwitchPreference( + "revanced_custom_hide_shorts", + ), + ListPreference( + key = "revanced_custom_default_playback_speed", + tag = "app.revanced.extension.shared.settings.preference.SortedListPreference", + ), + SwitchPreference( + "revanced_custom_remember_playback_speed", + ), + ListPreference( + key = "revanced_custom_default_video_quality", + tag = "app.revanced.extension.shared.settings.preference.SortedListPreference", + ), + SwitchPreference( + "revanced_custom_remember_video_quality", + ), + SwitchPreference( + "revanced_custom_force_highest_quality", + ), + SwitchPreference( + "revanced_custom_debug_video_info", + ), + TextPreference( + key = "revanced_custom_app_name", + titleKey = "revanced_custom_app_name_title", + inputType = InputType.TEXT_CAP_CHARACTERS, + tag = "app.revanced.extension.shared.settings.preference.TextInputPreference", + ), + ListPreference( + key = "revanced_custom_startup_tab", + tag = "app.revanced.extension.shared.settings.preference.SortedListPreference", + ), + SwitchPreference( + "revanced_custom_clean_urls", + ), + ), + ), TextPreference( key = null, titleKey = "revanced_pref_import_export_title", @@ -230,7 +289,7 @@ val settingsPatch = bytecodePatch( // Update shared dark mode status based on YT theme. // This is needed because YT allows forcing light/dark mode // which then differs from the system dark mode status. - setThemeMethod.apply { + (if (is_21_34_or_greater) setThemeMethod21_34 else setThemeMethod).apply { findInstructionIndicesReversedOrThrow(Opcode.RETURN_OBJECT).forEach { index -> val register = getInstruction(index).registerA addInstructionsAtControlFlowLabel( @@ -241,10 +300,12 @@ val settingsPatch = bytecodePatch( } // Add setting to force Cairo settings fragment on/off. - cairoFragmentConfigMethodMatch.method.insertLiteralOverride( - cairoFragmentConfigMethodMatch[0], - "$YOUTUBE_ACTIVITY_HOOK_CLASS_DESCRIPTOR->useCairoSettingsFragment(Z)Z", - ) + (if (is_21_34_or_greater) cairoFragmentConfigMethodMatch21_34 else cairoFragmentConfigMethodMatch).let { + it.method.insertLiteralOverride( + it[0], + "$YOUTUBE_ACTIVITY_HOOK_CLASS_DESCRIPTOR->useCairoSettingsFragment(Z)Z", + ) + } // Bold icon resources are found starting in 20.23, but many YT icons are not bold. // 20.31 is the first version that seems to have all the bold icons. diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/information/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/information/Fingerprints.kt index e5b5657a5..d62242db8 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/video/information/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/information/Fingerprints.kt @@ -183,6 +183,32 @@ internal val BytecodePatchContext.playbackSpeedClassMethod by gettingFirstMethod opcodes(Opcode.RETURN_OBJECT) } +/** + * YouTube 21.34+ playback-speed bottom sheet fragment factory `Lpzy;->aW(Lce;Ljava/lang/String;)Lpzy;`. + * + * In 21.34 the `PLAYBACK_RATE_MENU_BOTTOM_SHEET_FRAGMENT` string moved into the large state-holder + * factory `Lhxz;->p()Ljava/lang/Object;` (which returns `Ljava/lang/Object;`, not the fragment type), + * so the legacy `playbackSpeedClassMethod` fingerprint no longer resolves. The fragment is instead + * obtained from its static factory, lookup by tag with fallback creation, which returns the exact + * fragment instance (`Lpzy;`). It is uniquely identified by the exact return type `Lpzy;` together + * with the `getSupportFragmentManager` -> `f(Ljava/lang/String;)L` lookup sequence. + */ +internal val BytecodePatchContext.playbackSpeedClassMethod21_34 by gettingFirstMethodDeclaratively { + accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC) + returnType("Lpzy;") + parameterTypes("L", "Ljava/lang/String;") + instructions( + method("getSupportFragmentManager"), + after(Opcode.MOVE_RESULT_OBJECT()), + after(method { + returnType.startsWith("L") && parameterTypes.size == 1 && parameterTypes.first() == "Ljava/lang/String;" + }), + after(Opcode.MOVE_RESULT_OBJECT()), + after(Opcode.IF_EQZ()), + after(Opcode.CHECK_CAST()), + ) +} + /** * YouTube 20.19 and lower. */ diff --git a/patches/src/main/resources/addresources/values/strings.xml b/patches/src/main/resources/addresources/values/strings.xml index 28a4d7166..993a57ea5 100644 --- a/patches/src/main/resources/addresources/values/strings.xml +++ b/patches/src/main/resources/addresources/values/strings.xml @@ -1516,6 +1516,7 @@ Limitation: Using the back button on the toolbar may not work" Modern 2 Modern 3 Modern 4 + Selecting a miniplayer type is no longer available on your YouTube version. Disable rounded corners Corners are square Corners are rounded @@ -1944,6 +1945,36 @@ Video playback with AV1 may stutter or drop frames." Debug logging Debug logs are enabled Debug logs are disabled + + Custom + Reset Custom Settings + Restore custom preferences to defaults + Force AMOLED + Use true black in the YouTube UI + Pure Black Player + Make player background pure black + Disable Ambient Mode + Prevent Ambient Mode from activating + Hide Shorts + Remove Shorts UI entries + Default Playback Speed + Select default playback speed + Remember Playback Speed + Remember speed across videos + Default Video Quality + Select default video quality + Remember Video Quality + Remember quality across videos + Force Highest Available Quality + Prefer highest quality option + Debug Video Information + Show technical video details + Custom App Name + Change the visible application name + Default Startup Tab + Select startup tab + Clean YouTube URLs + Remove tracking parameters from shared links