From 1151a9a5be702487ea91be2cff3700c8625f79c0 Mon Sep 17 00:00:00 2001 From: inotia00 <108592928+inotia00@users.noreply.github.com> Date: Wed, 29 Jan 2025 19:25:07 +0900 Subject: [PATCH] fix(YouTube - Change live ring click action): Channel does not open when the live ring of Shorts live stream is clicked --- .../general/OpenChannelOfLiveAvatarPatch.java | 86 ++++++++++-------- .../com/facebook/litho/ComponentHost.java | 19 ++++ .../livering/OpenChannelOfLiveAvatarPatch.kt | 90 +++++++++++++++++-- .../youtube/shorts/components/Fingerprints.kt | 38 -------- .../shorts/components/ShortsComponentPatch.kt | 6 +- .../video/playbackstart/Fingerprints.kt | 40 +++++++++ .../PlaybackStartDescriptorPatch.kt | 4 +- .../youtube/settings/host/values/strings.xml | 2 +- 8 files changed, 197 insertions(+), 88 deletions(-) create mode 100644 extensions/shared/stub/src/main/java/com/facebook/litho/ComponentHost.java diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/OpenChannelOfLiveAvatarPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/OpenChannelOfLiveAvatarPatch.java index 448a94f54..60ddd9a54 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/OpenChannelOfLiveAvatarPatch.java +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/OpenChannelOfLiveAvatarPatch.java @@ -1,9 +1,10 @@ package app.revanced.extension.youtube.patches.general; -import androidx.annotation.Nullable; +import androidx.annotation.NonNull; + +import com.facebook.litho.ComponentHost; import java.util.Map; -import java.util.concurrent.atomic.AtomicBoolean; import app.revanced.extension.shared.utils.Logger; import app.revanced.extension.youtube.patches.general.requests.VideoDetailsRequest; @@ -15,37 +16,70 @@ public final class OpenChannelOfLiveAvatarPatch { private static final boolean CHANGE_LIVE_RING_CLICK_ACTION = Settings.CHANGE_LIVE_RING_CLICK_ACTION.get(); - private static final AtomicBoolean engagementPanelOpen = new AtomicBoolean(false); private static volatile String videoId = ""; + /** + * This key's value is the LithoView that opened the video (Live ring or Thumbnails). + */ + private static final String ELEMENTS_SENDER_VIEW = + "com.google.android.libraries.youtube.rendering.elements.sender_view"; + /** * If the video is open by clicking live ring, this key does not exists. */ private static final String VIDEO_THUMBNAIL_VIEW_KEY = "VideoPresenterConstants.VIDEO_THUMBNAIL_VIEW_KEY"; - public static void showEngagementPanel(@Nullable Object object) { - engagementPanelOpen.set(object != null); + /** + * Injection point. + * + * @param playbackStartDescriptorMap map containing information about PlaybackStartDescriptor + * @param newlyLoadedVideoId id of the current video + */ + public static void fetchChannelId(@NonNull Map playbackStartDescriptorMap, String newlyLoadedVideoId) { + try { + if (!CHANGE_LIVE_RING_CLICK_ACTION) { + return; + } + // Video id is empty + if (newlyLoadedVideoId.isEmpty()) { + return; + } + // Video was opened by clicking the thumbnail + if (playbackStartDescriptorMap.containsKey(VIDEO_THUMBNAIL_VIEW_KEY)) { + return; + } + // If the video was opened in the watch history, there is no VIDEO_THUMBNAIL_VIEW_KEY + // In this case, check the view that opened the video (Live ring is litho) + if (!(playbackStartDescriptorMap.get(ELEMENTS_SENDER_VIEW) instanceof ComponentHost componentHost)) { + return; + } + // Child count of other litho Views such as Thumbnail and Watch history: 2 + // Child count of live ring: 1 + if (componentHost.getChildCount() != 1) { + return; + } + // Fetch channel id + videoId = newlyLoadedVideoId; + VideoDetailsRequest.fetchRequestIfNeeded(newlyLoadedVideoId); + } catch (Exception ex) { + Logger.printException(() -> "fetchVideoInformation failure", ex); + } } - public static void hideEngagementPanel() { - engagementPanelOpen.compareAndSet(true, false); - } - - public static boolean openChannelOfLiveAvatar() { + public static boolean openChannel() { try { if (!CHANGE_LIVE_RING_CLICK_ACTION) { return false; } - if (engagementPanelOpen.get()) { - return false; - } + // If it is not fetch, the video id is empty if (videoId.isEmpty()) { return false; } VideoDetailsRequest request = VideoDetailsRequest.getRequestForVideoId(videoId); if (request != null) { String channelId = request.getInfo(); + // Open the channel if (channelId != null) { videoId = ""; VideoUtils.openChannel(channelId); @@ -53,33 +87,9 @@ public final class OpenChannelOfLiveAvatarPatch { } } } catch (Exception ex) { - Logger.printException(() -> "openChannelOfLiveAvatar failure", ex); + Logger.printException(() -> "openChannel failure", ex); } return false; } - public static void openChannelOfLiveAvatar(Map playbackStartDescriptorMap, String newlyLoadedVideoId) { - try { - if (!CHANGE_LIVE_RING_CLICK_ACTION) { - return; - } - if (playbackStartDescriptorMap == null) { - return; - } - if (playbackStartDescriptorMap.containsKey(VIDEO_THUMBNAIL_VIEW_KEY)) { - return; - } - if (engagementPanelOpen.get()) { - return; - } - if (newlyLoadedVideoId.isEmpty()) { - return; - } - videoId = newlyLoadedVideoId; - VideoDetailsRequest.fetchRequestIfNeeded(newlyLoadedVideoId); - } catch (Exception ex) { - Logger.printException(() -> "openChannelOfLiveAvatar failure", ex); - } - } - } diff --git a/extensions/shared/stub/src/main/java/com/facebook/litho/ComponentHost.java b/extensions/shared/stub/src/main/java/com/facebook/litho/ComponentHost.java new file mode 100644 index 000000000..49e69e237 --- /dev/null +++ b/extensions/shared/stub/src/main/java/com/facebook/litho/ComponentHost.java @@ -0,0 +1,19 @@ +package com.facebook.litho; + +import android.content.Context; +import android.view.ViewGroup; + +/** + * "CompileOnly" class + *

+ * This class will not be included and "replaced" by the real package's class. + */ +public class ComponentHost extends ViewGroup { + public ComponentHost(Context context) { + super(context); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/general/livering/OpenChannelOfLiveAvatarPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/general/livering/OpenChannelOfLiveAvatarPatch.kt index dc511073e..3cd06f189 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/general/livering/OpenChannelOfLiveAvatarPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/general/livering/OpenChannelOfLiveAvatarPatch.kt @@ -4,15 +4,19 @@ import app.revanced.patcher.extensions.InstructionExtensions.addInstructions import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels import app.revanced.patcher.extensions.InstructionExtensions.getInstruction import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE -import app.revanced.patches.youtube.utils.engagement.engagementPanelHookPatch -import app.revanced.patches.youtube.utils.engagement.hookEngagementPanelState import app.revanced.patches.youtube.utils.extension.Constants.GENERAL_PATH import app.revanced.patches.youtube.utils.patch.PatchList.CHANGE_LIVE_RING_CLICK_ACTION +import app.revanced.patches.youtube.utils.playservice.is_19_25_or_greater +import app.revanced.patches.youtube.utils.playservice.versionCheckPatch import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference import app.revanced.patches.youtube.utils.settings.settingsPatch +import app.revanced.patches.youtube.video.playbackstart.PLAYBACK_START_DESCRIPTOR_CLASS_DESCRIPTOR import app.revanced.patches.youtube.video.playbackstart.playbackStartDescriptorPatch import app.revanced.patches.youtube.video.playbackstart.playbackStartVideoIdReference +import app.revanced.patches.youtube.video.playbackstart.shortsPlaybackStartIntentFingerprint +import app.revanced.patches.youtube.video.playbackstart.shortsPlaybackStartIntentLegacyFingerprint import app.revanced.util.fingerprint.methodOrThrow import app.revanced.util.getReference import app.revanced.util.indexOfFirstInstructionOrThrow @@ -20,7 +24,10 @@ import app.revanced.util.indexOfFirstInstructionReversedOrThrow 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.ReferenceInstruction +import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction import com.android.tools.smali.dexlib2.iface.reference.MethodReference +import com.android.tools.smali.dexlib2.iface.reference.TypeReference private const val EXTENSION_CLASS_DESCRIPTOR = "$GENERAL_PATH/OpenChannelOfLiveAvatarPatch;" @@ -35,13 +42,11 @@ val openChannelOfLiveAvatarPatch = bytecodePatch( dependsOn( settingsPatch, playbackStartDescriptorPatch, - engagementPanelHookPatch, + versionCheckPatch, ) execute { - hookEngagementPanelState(EXTENSION_CLASS_DESCRIPTOR) - clientSettingEndpointFingerprint.methodOrThrow().apply { val eqzIndex = indexOfFirstInstructionReversedOrThrow(Opcode.IF_EQZ) var freeIndex = indexOfFirstInstructionReversedOrThrow(eqzIndex, Opcode.NEW_INSTANCE) @@ -49,7 +54,7 @@ val openChannelOfLiveAvatarPatch = bytecodePatch( addInstructionsWithLabels( eqzIndex, """ - invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->openChannelOfLiveAvatar()Z + invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->openChannel()Z move-result v$freeRegister if-eqz v$freeRegister, :ignore return-void @@ -76,11 +81,82 @@ val openChannelOfLiveAvatarPatch = bytecodePatch( playbackStartIndex + 1, """ invoke-virtual { v$playbackStartRegister }, $playbackStartVideoIdReference move-result-object v$freeRegister - invoke-static { v$mapRegister, v$freeRegister }, $EXTENSION_CLASS_DESCRIPTOR->openChannelOfLiveAvatar(Ljava/util/Map;Ljava/lang/String;)V + invoke-static { v$mapRegister, v$freeRegister }, $EXTENSION_CLASS_DESCRIPTOR->fetchChannelId(Ljava/util/Map;Ljava/lang/String;)V """ ) } + fun MutableMethod.openChannel() = + implementation!!.instructions + .withIndex() + .filter { (_, instruction) -> + val reference = (instruction as? ReferenceInstruction)?.reference + instruction.opcode == Opcode.NEW_INSTANCE && + reference is TypeReference && + reference.type == "Landroid/os/Bundle;" + } + .map { (index, _) -> index } + .reversed() + .forEach { index -> + val register = getInstruction(index).registerA + + addInstructionsWithLabels( + index, """ + invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->openChannel()Z + move-result v$register + if-eqz v$register, :ignore + return-void + :ignore + nop + """ + ) + } + + + fun fetchChannelIdInstructions( + playbackStartRegister: Int, + mapRegister: Int, + videoIdRegister: Int, + ) = + """ + invoke-virtual { v$playbackStartRegister }, $playbackStartVideoIdReference + move-result-object v$videoIdRegister + invoke-static { v$mapRegister, v$videoIdRegister }, $EXTENSION_CLASS_DESCRIPTOR->fetchChannelId(Ljava/util/Map;Ljava/lang/String;)V + """ + + if (is_19_25_or_greater) { + shortsPlaybackStartIntentFingerprint.methodOrThrow().apply { + openChannel() + + addInstructionsWithLabels( + 0, """ + move-object/from16 v0, p1 + move-object/from16 v1, p2 + ${fetchChannelIdInstructions(0, 1, 2)} + """ + ) + } + } else { + shortsPlaybackStartIntentLegacyFingerprint.methodOrThrow().apply { + openChannel() + + val playbackStartIndex = indexOfFirstInstructionOrThrow { + getReference()?.returnType == PLAYBACK_START_DESCRIPTOR_CLASS_DESCRIPTOR + } + val mapIndex = indexOfFirstInstructionReversedOrThrow(playbackStartIndex, Opcode.IPUT) + val mapRegister = getInstruction(mapIndex).registerA + val playbackStartRegister = getInstruction(playbackStartIndex + 1).registerA + val videoIdRegister = getInstruction(playbackStartIndex).registerC + + addInstructionsWithLabels( + playbackStartIndex + 2, """ + move-object/from16 v$mapRegister, p2 + ${fetchChannelIdInstructions(playbackStartRegister, mapRegister, videoIdRegister)} + """ + ) + } + } + // region add settings addPreference( diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/shorts/components/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/shorts/components/Fingerprints.kt index 573c63f4a..c6cdc6329 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/shorts/components/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/shorts/components/Fingerprints.kt @@ -187,41 +187,3 @@ internal val shortsFullscreenFeatureFingerprint = legacyFingerprint( accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, literals = listOf(FULLSCREEN_FEATURE_FLAG), ) - -// Pre 19.25 -internal val shortsPlaybackIntentLegacyFingerprint = legacyFingerprint( - name = "shortsPlaybackIntentLegacyFingerprint", - returnType = "V", - parameters = listOf( - "L", - "Ljava/util/Map;", - "J", - "Ljava/lang/String;", - "Z", - "Ljava/util/Map;" - ), - strings = listOf( - // None of these strings are unique. - "com.google.android.apps.youtube.app.endpoint.flags", - "ReelWatchFragmentArgs", - "reels_fragment_descriptor" - ) -) - -internal val shortsPlaybackIntentFingerprint = legacyFingerprint( - name = "shortsPlaybackIntentFingerprint", - accessFlags = AccessFlags.PROTECTED or AccessFlags.FINAL, - returnType = "V", - parameters = listOf( - "Lcom/google/android/libraries/youtube/player/model/PlaybackStartDescriptor;", - "Ljava/util/Map;", - "J", - "Ljava/lang/String;" - ), - strings = listOf( - // None of these strings are unique. - "com.google.android.apps.youtube.app.endpoint.flags", - "ReelWatchFragmentArgs", - "reels_fragment_descriptor" - ) -) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/shorts/components/ShortsComponentPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/shorts/components/ShortsComponentPatch.kt index 8d7b386d4..09cb57cac 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/shorts/components/ShortsComponentPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/shorts/components/ShortsComponentPatch.kt @@ -65,6 +65,8 @@ import app.revanced.patches.youtube.video.information.videoInformationPatch import app.revanced.patches.youtube.video.playbackstart.PLAYBACK_START_DESCRIPTOR_CLASS_DESCRIPTOR import app.revanced.patches.youtube.video.playbackstart.playbackStartDescriptorPatch import app.revanced.patches.youtube.video.playbackstart.playbackStartVideoIdReference +import app.revanced.patches.youtube.video.playbackstart.shortsPlaybackStartIntentFingerprint +import app.revanced.patches.youtube.video.playbackstart.shortsPlaybackStartIntentLegacyFingerprint import app.revanced.patches.youtube.video.videoid.hookPlayerResponseVideoId import app.revanced.patches.youtube.video.videoid.videoIdPatch import app.revanced.util.REGISTER_TEMPLATE_REPLACEMENT @@ -894,7 +896,7 @@ val shortsComponentPatch = bytecodePatch( """ if (is_19_25_or_greater) { - shortsPlaybackIntentFingerprint.methodOrThrow().addInstructionsWithLabels( + shortsPlaybackStartIntentFingerprint.methodOrThrow().addInstructionsWithLabels( 0, """ move-object/from16 v0, p1 @@ -902,7 +904,7 @@ val shortsComponentPatch = bytecodePatch( """ ) } else { - shortsPlaybackIntentLegacyFingerprint.methodOrThrow().apply { + shortsPlaybackStartIntentLegacyFingerprint.methodOrThrow().apply { val index = indexOfFirstInstructionOrThrow { getReference()?.returnType == PLAYBACK_START_DESCRIPTOR_CLASS_DESCRIPTOR } diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/playbackstart/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/playbackstart/Fingerprints.kt index c196c1f44..db140c52a 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/video/playbackstart/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/playbackstart/Fingerprints.kt @@ -1,6 +1,8 @@ package app.revanced.patches.youtube.video.playbackstart import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags const val PLAYBACK_START_DESCRIPTOR_CLASS_DESCRIPTOR = "Lcom/google/android/libraries/youtube/player/model/PlaybackStartDescriptor;" @@ -16,4 +18,42 @@ internal val playbackStartFeatureFlagFingerprint = legacyFingerprint( literals = listOf(45380134L) ) +internal val shortsPlaybackStartIntentFingerprint = legacyFingerprint( + name = "shortsPlaybackStartIntentFingerprint", + accessFlags = AccessFlags.PROTECTED or AccessFlags.FINAL, + returnType = "V", + parameters = listOf( + "Lcom/google/android/libraries/youtube/player/model/PlaybackStartDescriptor;", + "Ljava/util/Map;", + "J", + "Ljava/lang/String;" + ), + strings = listOf( + // None of these strings are unique. + "com.google.android.apps.youtube.app.endpoint.flags", + "ReelWatchFragmentArgs", + "reels_fragment_descriptor" + ) +) + +// Pre 19.25 +internal val shortsPlaybackStartIntentLegacyFingerprint = legacyFingerprint( + name = "shortsPlaybackStartIntentLegacyFingerprint", + returnType = "V", + parameters = listOf( + "L", + "Ljava/util/Map;", + "J", + "Ljava/lang/String;", + "Z", + "Ljava/util/Map;" + ), + strings = listOf( + // None of these strings are unique. + "com.google.android.apps.youtube.app.endpoint.flags", + "ReelWatchFragmentArgs", + "reels_fragment_descriptor" + ) +) + diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/playbackstart/PlaybackStartDescriptorPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/playbackstart/PlaybackStartDescriptorPatch.kt index c2dbc260a..1b4e38b32 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/video/playbackstart/PlaybackStartDescriptorPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/playbackstart/PlaybackStartDescriptorPatch.kt @@ -2,7 +2,7 @@ package app.revanced.patches.youtube.video.playbackstart import app.revanced.patcher.extensions.InstructionExtensions.getInstruction import app.revanced.patcher.patch.bytecodePatch -import app.revanced.patches.youtube.utils.resourceid.sharedResourceIdPatch +import app.revanced.patches.youtube.utils.extension.sharedExtensionPatch import app.revanced.util.fingerprint.methodOrThrow import app.revanced.util.getReference import app.revanced.util.indexOfFirstInstructionOrThrow @@ -15,7 +15,7 @@ internal lateinit var playbackStartVideoIdReference: Reference val playbackStartDescriptorPatch = bytecodePatch( description = "playbackStartDescriptorPatch" ) { - dependsOn(sharedResourceIdPatch) + dependsOn(sharedExtensionPatch) execute { // Find the obfuscated method name for PlaybackStartDescriptor.videoId() diff --git a/patches/src/main/resources/youtube/settings/host/values/strings.xml b/patches/src/main/resources/youtube/settings/host/values/strings.xml index 477cf9c8b..5d837ef61 100644 --- a/patches/src/main/resources/youtube/settings/host/values/strings.xml +++ b/patches/src/main/resources/youtube/settings/host/values/strings.xml @@ -451,7 +451,7 @@ This does not bypass the age restriction. It just accepts it automatically."Change live ring click action "Channel opens when the live ring is clicked. -Limitation: Live ring of the Vertical live streams (Shorts live streams) cannot open a channel." +Limitation: When the Shorts live stream is opened in regular player due to the 'Open Shorts in regular player' setting, channel does not open." Live stream opens when the live ring is clicked. Spoof app version Version spoofed