fix(YouTube - Change live ring click action): Clicking on the timestamp in the comments opens the channel

This commit is contained in:
inotia00 2025-01-22 13:06:06 +09:00
parent 78f1d962cd
commit da770b32bf
10 changed files with 119 additions and 118 deletions

View File

@ -1,5 +1,9 @@
package app.revanced.extension.youtube.patches.general; package app.revanced.extension.youtube.patches.general;
import androidx.annotation.Nullable;
import java.util.concurrent.atomic.AtomicBoolean;
import app.revanced.extension.shared.utils.Logger; import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.youtube.patches.general.requests.VideoDetailsRequest; import app.revanced.extension.youtube.patches.general.requests.VideoDetailsRequest;
import app.revanced.extension.youtube.settings.Settings; import app.revanced.extension.youtube.settings.Settings;
@ -10,16 +14,16 @@ public final class OpenChannelOfLiveAvatarPatch {
private static final boolean CHANGE_LIVE_RING_CLICK_ACTION = private static final boolean CHANGE_LIVE_RING_CLICK_ACTION =
Settings.CHANGE_LIVE_RING_CLICK_ACTION.get(); Settings.CHANGE_LIVE_RING_CLICK_ACTION.get();
private static volatile String videoId = ""; private static final AtomicBoolean engagementPanelOpen = new AtomicBoolean(false);
private static volatile boolean isCommentsPanelOpen = false;
private static volatile boolean liveChannelAvatarClicked = false; private static volatile boolean liveChannelAvatarClicked = false;
private static volatile String videoId = "";
public static void commentsPanelClosed() { public static void showEngagementPanel(@Nullable Object object) {
isCommentsPanelOpen = false; engagementPanelOpen.set(object != null);
} }
public static void commentsPanelOpen() { public static void hideEngagementPanel() {
isCommentsPanelOpen = true; engagementPanelOpen.compareAndSet(true, false);
} }
public static void liveChannelAvatarClicked() { public static void liveChannelAvatarClicked() {
@ -34,13 +38,14 @@ public final class OpenChannelOfLiveAvatarPatch {
if (!liveChannelAvatarClicked) { if (!liveChannelAvatarClicked) {
return false; return false;
} }
if (isCommentsPanelOpen) { if (engagementPanelOpen.get()) {
return false; return false;
} }
VideoDetailsRequest request = VideoDetailsRequest.getRequestForVideoId(videoId); VideoDetailsRequest request = VideoDetailsRequest.getRequestForVideoId(videoId);
if (request != null) { if (request != null) {
String channelId = request.getInfo(); String channelId = request.getInfo();
if (channelId != null) { if (channelId != null) {
videoId = "";
liveChannelAvatarClicked = false; liveChannelAvatarClicked = false;
VideoUtils.openChannel(channelId); VideoUtils.openChannel(channelId);
return true; return true;
@ -60,7 +65,7 @@ public final class OpenChannelOfLiveAvatarPatch {
if (!liveChannelAvatarClicked) { if (!liveChannelAvatarClicked) {
return; return;
} }
if (isCommentsPanelOpen) { if (engagementPanelOpen.get()) {
return; return;
} }
if (newlyLoadedVideoId.isEmpty()) { if (newlyLoadedVideoId.isEmpty()) {

View File

@ -7,14 +7,14 @@ import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWith
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
import app.revanced.patcher.patch.PatchException import app.revanced.patcher.patch.PatchException
import app.revanced.patcher.patch.bytecodePatch import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
import app.revanced.patcher.util.smali.ExternalLabel import app.revanced.patcher.util.smali.ExternalLabel
import app.revanced.patches.shared.litho.addLithoFilter import app.revanced.patches.shared.litho.addLithoFilter
import app.revanced.patches.shared.litho.emptyComponentLabel import app.revanced.patches.shared.litho.emptyComponentLabel
import app.revanced.patches.shared.mainactivity.onCreateMethod import app.revanced.patches.shared.mainactivity.onCreateMethod
import app.revanced.patches.youtube.utils.bottomsheet.bottomSheetHookPatch import app.revanced.patches.youtube.utils.bottomsheet.bottomSheetHookPatch
import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE
import app.revanced.patches.youtube.utils.engagementPanelBuilderFingerprint import app.revanced.patches.youtube.utils.engagement.engagementPanelHookPatch
import app.revanced.patches.youtube.utils.engagement.hookEngagementPanelState
import app.revanced.patches.youtube.utils.extension.Constants.COMPONENTS_PATH import app.revanced.patches.youtube.utils.extension.Constants.COMPONENTS_PATH
import app.revanced.patches.youtube.utils.extension.Constants.FEED_CLASS_DESCRIPTOR import app.revanced.patches.youtube.utils.extension.Constants.FEED_CLASS_DESCRIPTOR
import app.revanced.patches.youtube.utils.extension.Constants.FEED_PATH import app.revanced.patches.youtube.utils.extension.Constants.FEED_PATH
@ -41,19 +41,16 @@ import app.revanced.util.fingerprint.methodOrThrow
import app.revanced.util.fingerprint.mutableClassOrThrow import app.revanced.util.fingerprint.mutableClassOrThrow
import app.revanced.util.getReference import app.revanced.util.getReference
import app.revanced.util.getWalkerMethod import app.revanced.util.getWalkerMethod
import app.revanced.util.indexOfFirstInstruction
import app.revanced.util.indexOfFirstInstructionOrThrow import app.revanced.util.indexOfFirstInstructionOrThrow
import app.revanced.util.indexOfFirstInstructionReversedOrThrow import app.revanced.util.indexOfFirstInstructionReversedOrThrow
import app.revanced.util.indexOfFirstLiteralInstructionOrThrow import app.revanced.util.indexOfFirstLiteralInstructionOrThrow
import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.Method
import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction 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.OneRegisterInstruction
import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction 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.instruction.TwoRegisterInstruction
import com.android.tools.smali.dexlib2.iface.reference.MethodReference import com.android.tools.smali.dexlib2.iface.reference.MethodReference
import com.android.tools.smali.dexlib2.iface.reference.StringReference import com.android.tools.smali.dexlib2.iface.reference.StringReference
import com.android.tools.smali.dexlib2.util.MethodUtil
private const val CAROUSEL_SHELF_FILTER_CLASS_DESCRIPTOR = private const val CAROUSEL_SHELF_FILTER_CLASS_DESCRIPTOR =
"$COMPONENTS_PATH/CarouselShelfFilter;" "$COMPONENTS_PATH/CarouselShelfFilter;"
@ -82,6 +79,7 @@ val feedComponentsPatch = bytecodePatch(
sharedResourceIdPatch, sharedResourceIdPatch,
settingsPatch, settingsPatch,
bottomSheetHookPatch, bottomSheetHookPatch,
engagementPanelHookPatch,
versionCheckPatch, versionCheckPatch,
) )
execute { execute {
@ -176,38 +174,6 @@ val feedComponentsPatch = bytecodePatch(
// region patch for hide relative video // region patch for hide relative video
fun Method.indexOfEngagementPanelBuilderInstruction(targetMethod: MutableMethod) =
indexOfFirstInstruction {
opcode == Opcode.INVOKE_DIRECT &&
MethodUtil.methodSignaturesMatch(
targetMethod,
getReference<MethodReference>()!!
)
}
engagementPanelBuilderFingerprint.matchOrThrow().let {
it.classDef.methods.filter { method ->
method.indexOfEngagementPanelBuilderInstruction(it.method) >= 0
}.forEach { method ->
method.apply {
val index = indexOfEngagementPanelBuilderInstruction(it.method)
val register = getInstruction<OneRegisterInstruction>(index + 1).registerA
addInstruction(
index + 2,
"invoke-static {v$register}, " +
"$RELATED_VIDEO_CLASS_DESCRIPTOR->showEngagementPanel(Ljava/lang/Object;)V"
)
}
}
}
engagementPanelUpdateFingerprint.methodOrThrow(engagementPanelBuilderFingerprint)
.addInstruction(
0,
"invoke-static {}, $RELATED_VIDEO_CLASS_DESCRIPTOR->hideEngagementPanel()V"
)
linearLayoutManagerItemCountsFingerprint.matchOrThrow().let { linearLayoutManagerItemCountsFingerprint.matchOrThrow().let {
val methodWalker = val methodWalker =
it.getWalkerMethod(it.patternMatch!!.endIndex) it.getWalkerMethod(it.patternMatch!!.endIndex)
@ -224,6 +190,8 @@ val feedComponentsPatch = bytecodePatch(
} }
} }
hookEngagementPanelState(RELATED_VIDEO_CLASS_DESCRIPTOR)
// endregion // endregion
// region patch for hide subscriptions channel section for tablet // region patch for hide subscriptions channel section for tablet

View File

@ -11,12 +11,9 @@ import app.revanced.patches.youtube.utils.resourceid.filterBarHeight
import app.revanced.patches.youtube.utils.resourceid.horizontalCardList import app.revanced.patches.youtube.utils.resourceid.horizontalCardList
import app.revanced.patches.youtube.utils.resourceid.relatedChipCloudMargin import app.revanced.patches.youtube.utils.resourceid.relatedChipCloudMargin
import app.revanced.util.fingerprint.legacyFingerprint import app.revanced.util.fingerprint.legacyFingerprint
import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstruction
import app.revanced.util.or import app.revanced.util.or
import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
internal val breakingNewsFingerprint = legacyFingerprint( internal val breakingNewsFingerprint = legacyFingerprint(
name = "breakingNewsFingerprint", name = "breakingNewsFingerprint",
@ -103,19 +100,6 @@ internal val elementParserParentFingerprint = legacyFingerprint(
strings = listOf("Element tree missing id in debug mode.") strings = listOf("Element tree missing id in debug mode.")
) )
internal val engagementPanelUpdateFingerprint = legacyFingerprint(
name = "engagementPanelUpdateFingerprint",
returnType = "V",
accessFlags = AccessFlags.PRIVATE or AccessFlags.FINAL,
parameters = listOf("L", "Z"),
customFingerprint = { method, _ ->
method.indexOfFirstInstruction {
opcode == Opcode.INVOKE_VIRTUAL &&
getReference<MethodReference>().toString() == "Ljava/util/ArrayDeque;->pop()Ljava/lang/Object;"
} >= 0
}
)
internal val filterBarHeightFingerprint = legacyFingerprint( internal val filterBarHeightFingerprint = legacyFingerprint(
name = "filterBarHeightFingerprint", name = "filterBarHeightFingerprint",
returnType = "V", returnType = "V",

View File

@ -40,39 +40,3 @@ internal fun indexOfPlaybackStartDescriptorInstruction(method: Method) =
reference?.returnType == "Lcom/google/android/libraries/youtube/player/model/PlaybackStartDescriptor;" && reference?.returnType == "Lcom/google/android/libraries/youtube/player/model/PlaybackStartDescriptor;" &&
reference.parameterTypes.isEmpty() reference.parameterTypes.isEmpty()
} }
internal val engagementPanelCommentsClosedFingerprint = legacyFingerprint(
name = "engagementPanelCommentsClosedFingerprint",
returnType = "V",
accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
parameters = emptyList(),
opcodes = listOf(
Opcode.IGET_OBJECT,
Opcode.INVOKE_STATIC,
Opcode.IGET_OBJECT,
Opcode.INVOKE_DIRECT,
),
customFingerprint = { method, _ ->
method.indexOfFirstInstruction {
opcode == Opcode.INVOKE_INTERFACE &&
getReference<MethodReference>()?.name == "hasNext"
} >= 0
}
)
internal val engagementPanelCommentsOpenFingerprint = legacyFingerprint(
name = "engagementPanelCommentsOpenFingerprint",
returnType = "V",
accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
parameters = listOf("L"),
opcodes = listOf(
Opcode.IGET_OBJECT,
Opcode.IF_NE,
Opcode.RETURN_VOID,
Opcode.IPUT_OBJECT,
Opcode.RETURN_VOID,
),
customFingerprint = { method, _ ->
method.implementation!!.instructions.count() == 5
}
)

View File

@ -6,7 +6,8 @@ import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWith
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
import app.revanced.patcher.patch.bytecodePatch import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE
import app.revanced.patches.youtube.utils.engagementPanelTitleParentFingerprint 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.extension.Constants.GENERAL_PATH
import app.revanced.patches.youtube.utils.patch.PatchList.CHANGE_LIVE_RING_CLICK_ACTION import app.revanced.patches.youtube.utils.patch.PatchList.CHANGE_LIVE_RING_CLICK_ACTION
import app.revanced.patches.youtube.utils.resourceid.sharedResourceIdPatch import app.revanced.patches.youtube.utils.resourceid.sharedResourceIdPatch
@ -31,30 +32,21 @@ val openChannelOfLiveAvatarPatch = bytecodePatch(
compatibleWith(COMPATIBLE_PACKAGE) compatibleWith(COMPATIBLE_PACKAGE)
dependsOn( dependsOn(
playbackStartDescriptorPatch,
sharedResourceIdPatch,
settingsPatch, settingsPatch,
sharedResourceIdPatch,
playbackStartDescriptorPatch,
engagementPanelHookPatch,
) )
execute { execute {
mapOf(
engagementPanelCommentsClosedFingerprint to "commentsPanelClosed",
engagementPanelCommentsOpenFingerprint to "commentsPanelOpen",
).forEach { (fingerprint, methodName) ->
fingerprint
.methodOrThrow(engagementPanelTitleParentFingerprint)
.addInstruction(
0,
"invoke-static {}, $EXTENSION_CLASS_DESCRIPTOR->$methodName()V"
)
}
elementsImageFingerprint.methodOrThrow().addInstruction( elementsImageFingerprint.methodOrThrow().addInstruction(
0, 0,
"invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->liveChannelAvatarClicked()V" "invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->liveChannelAvatarClicked()V"
) )
hookEngagementPanelState(EXTENSION_CLASS_DESCRIPTOR)
clientSettingEndpointFingerprint.methodOrThrow().apply { clientSettingEndpointFingerprint.methodOrThrow().apply {
val eqzIndex = indexOfFirstInstructionReversedOrThrow(Opcode.IF_EQZ) val eqzIndex = indexOfFirstInstructionReversedOrThrow(Opcode.IF_EQZ)
var freeIndex = indexOfFirstInstructionReversedOrThrow(eqzIndex, Opcode.NEW_INSTANCE) var freeIndex = indexOfFirstInstructionReversedOrThrow(eqzIndex, Opcode.NEW_INSTANCE)

View File

@ -9,7 +9,6 @@ import app.revanced.patcher.util.smali.ExternalLabel
import app.revanced.patches.shared.litho.addLithoFilter import app.revanced.patches.shared.litho.addLithoFilter
import app.revanced.patches.shared.litho.lithoFilterPatch import app.revanced.patches.shared.litho.lithoFilterPatch
import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE
import app.revanced.patches.youtube.utils.engagementPanelTitleParentFingerprint
import app.revanced.patches.youtube.utils.extension.Constants.COMPONENTS_PATH import app.revanced.patches.youtube.utils.extension.Constants.COMPONENTS_PATH
import app.revanced.patches.youtube.utils.extension.Constants.PLAYER_CLASS_DESCRIPTOR import app.revanced.patches.youtube.utils.extension.Constants.PLAYER_CLASS_DESCRIPTOR
import app.revanced.patches.youtube.utils.patch.PatchList.DESCRIPTION_COMPONENTS import app.revanced.patches.youtube.utils.patch.PatchList.DESCRIPTION_COMPONENTS

View File

@ -16,6 +16,11 @@ internal val engagementPanelTitleFingerprint = legacyFingerprint(
} }
) )
internal val engagementPanelTitleParentFingerprint = legacyFingerprint(
name = "engagementPanelTitleParentFingerprint",
strings = listOf("[EngagementPanelTitleHeader] Cannot remove action buttons from header as the child count is out of sync. Buttons to remove exceed current header child count.")
)
internal fun indexOfContentDescriptionInstruction(method: Method) = internal fun indexOfContentDescriptionInstruction(method: Method) =
method.indexOfFirstInstructionReversed { method.indexOfFirstInstructionReversed {
opcode == Opcode.INVOKE_VIRTUAL && opcode == Opcode.INVOKE_VIRTUAL &&

View File

@ -59,11 +59,6 @@ internal val engagementPanelBuilderFingerprint = legacyFingerprint(
) )
) )
internal val engagementPanelTitleParentFingerprint = legacyFingerprint(
name = "engagementPanelTitleParentFingerprint",
strings = listOf("[EngagementPanelTitleHeader] Cannot remove action buttons from header as the child count is out of sync. Buttons to remove exceed current header child count.")
)
internal val layoutConstructorFingerprint = legacyFingerprint( internal val layoutConstructorFingerprint = legacyFingerprint(
name = "layoutConstructorFingerprint", name = "layoutConstructorFingerprint",
returnType = "V", returnType = "V",

View File

@ -0,0 +1,67 @@
package app.revanced.patches.youtube.utils.engagement
import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
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.engagementPanelBuilderFingerprint
import app.revanced.patches.youtube.utils.resourceid.sharedResourceIdPatch
import app.revanced.util.fingerprint.matchOrThrow
import app.revanced.util.fingerprint.methodOrThrow
import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstructionReversed
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.Method
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
import com.android.tools.smali.dexlib2.util.MethodUtil
private lateinit var hideEngagementPanelMethod: MutableMethod
private var showEngagementPanelMethods = mutableListOf<MutableMethod>()
val engagementPanelHookPatch = bytecodePatch(
description = "engagementPanelHookPatch"
) {
dependsOn(sharedResourceIdPatch)
execute {
engagementPanelBuilderFingerprint.matchOrThrow().let {
it.classDef.methods.filter { method ->
method.indexOfEngagementPanelBuilderInstruction(it.method) >= 0
}.forEach { method ->
showEngagementPanelMethods.add(method)
}
}
hideEngagementPanelMethod =
engagementPanelUpdateFingerprint.methodOrThrow(engagementPanelBuilderFingerprint)
}
}
private fun Method.indexOfEngagementPanelBuilderInstruction(targetMethod: MutableMethod) =
indexOfFirstInstructionReversed {
opcode == Opcode.INVOKE_DIRECT &&
MethodUtil.methodSignaturesMatch(
targetMethod,
getReference<MethodReference>()!!
)
}
internal fun hookEngagementPanelState(classDescriptor: String) {
showEngagementPanelMethods.forEach { method ->
method.apply {
val index = indexOfEngagementPanelBuilderInstruction(this)
val register = getInstruction<OneRegisterInstruction>(index + 1).registerA
addInstruction(
index + 2,
"invoke-static {v$register}, $classDescriptor->showEngagementPanel(Ljava/lang/Object;)V"
)
}
}
hideEngagementPanelMethod.addInstruction(
0,
"invoke-static {}, $classDescriptor->hideEngagementPanel()V"
)
}

View File

@ -0,0 +1,22 @@
package app.revanced.patches.youtube.utils.engagement
import app.revanced.util.fingerprint.legacyFingerprint
import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstruction
import app.revanced.util.or
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
internal val engagementPanelUpdateFingerprint = legacyFingerprint(
name = "engagementPanelUpdateFingerprint",
returnType = "V",
accessFlags = AccessFlags.PRIVATE or AccessFlags.FINAL,
parameters = listOf("L", "Z"),
customFingerprint = { method, _ ->
method.indexOfFirstInstruction {
opcode == Opcode.INVOKE_VIRTUAL &&
getReference<MethodReference>().toString() == "Ljava/util/ArrayDeque;->pop()Ljava/lang/Object;"
} >= 0
}
)