From 97b4fc5b5a78bc5c3430531d6706cb56deff60dd Mon Sep 17 00:00:00 2001 From: whitehai11 Date: Thu, 3 Sep 2026 19:50:50 +0200 Subject: [PATCH] fix(YouTube): Update fingerprints and logic for YouTube 21.34, add custom settings Reworks fingerprint matching across ads, GMS core, litho filter, player controls, context hook, background playback, end screen cards, miniplayer, and Shorts autoplay patches to keep working on YouTube 21.34, where several underlying methods and resources changed. Also fixes video stream spoofing to build the request URI via Uri.Builder instead of the removed toString() path, and adds a new Custom settings screen with AMOLED/pure black player, ambient mode, Shorts hiding, playback speed/quality defaults, debug video info, custom app name, startup tab, and URL cleaning options. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01NXRsEcWGqDhsCbfEdrfCdE --- .../youtube/patches/litho/AdsFilter.java | 11 + .../patches/shared/misc/spoof/Fingerprints.kt | 11 +- .../misc/spoof/SpoofVideoStreamsPatch.kt | 17 +- .../youtube/ad/general/Fingerprints.kt | 24 +++ .../youtube/ad/general/HideAdsPatch.kt | 51 +++-- .../endscreencards/HideEndScreenCardsPatch.kt | 10 +- .../layout/miniplayer/MiniplayerPatch.kt | 204 +++++++++++------- .../shortsautoplay/ShortsAutoplayPatch.kt | 185 ++++++++-------- .../BackgroundPlaybackPatch.kt | 6 +- .../contexthook/ClientContextHookPatch.kt | 24 ++- .../patches/youtube/misc/gms/Fingerprints.kt | 6 +- .../misc/litho/filter/LithoFilterPatch.kt | 19 +- .../misc/playercontrols/Fingerprints.kt | 21 ++ .../playercontrols/PlayerControlsPatch.kt | 8 +- .../misc/playservice/VersionCheckPatch.kt | 3 + .../youtube/misc/settings/Fingerprints.kt | 40 ++++ .../youtube/misc/settings/SettingsPatch.kt | 71 +++++- .../youtube/video/information/Fingerprints.kt | 26 +++ .../resources/addresources/values/strings.xml | 31 +++ 19 files changed, 547 insertions(+), 221 deletions(-) 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