From 73b95a514355d1df5221b47074529cabfba2e0a6 Mon Sep 17 00:00:00 2001 From: Francesco Marastoni <49027005+Francesco146@users.noreply.github.com> Date: Sun, 15 Dec 2024 07:54:33 +0000 Subject: [PATCH] feat(YouTube - Shorts components): add `Custom actions in toolbar` setting (YouTube 18.38.44+) (#106) * feat(YouTube - Shorts Player): Hook `More` button * fix(YouTube - Shorts Player): Incorrect videoId when playing a Short * fix(YouTube - Shorts Player): `Open in normal player` didn't work when `Enable open links directly` was disabled * fix: Use an existing class --------- Co-authored-by: inotia00 <108592928+inotia00@users.noreply.github.com> --- .../patches/shorts/CustomActionsPatch.java | 93 +++++++++++++++++++ .../extension/youtube/settings/Settings.java | 11 ++- .../youtube/shorts/components/Fingerprints.kt | 21 +++++ .../shorts/components/ShortsComponentPatch.kt | 55 +++++++++-- .../youtube/settings/host/values/strings.xml | 9 ++ .../youtube/settings/xml/revanced_prefs.xml | 18 +++- 6 files changed, 191 insertions(+), 16 deletions(-) diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/shorts/CustomActionsPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/shorts/CustomActionsPatch.java index 062e2265a..95112a7be 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/shorts/CustomActionsPatch.java +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/shorts/CustomActionsPatch.java @@ -4,16 +4,20 @@ import static app.revanced.extension.shared.utils.ResourceUtils.getString; import static app.revanced.extension.youtube.patches.components.ShortsCustomActionsFilter.isShortsFlyoutMenuVisible; import static app.revanced.extension.youtube.utils.ExtendedUtils.isSpoofingToLessThan; +import android.app.AlertDialog; import android.content.Context; import android.graphics.drawable.Drawable; import android.support.v7.widget.RecyclerView; import android.view.View; import android.view.ViewGroup; +import android.widget.ImageView; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import org.apache.commons.lang3.StringUtils; + import java.lang.ref.WeakReference; import java.util.LinkedHashMap; import java.util.Map; @@ -34,12 +38,83 @@ public final class CustomActionsPatch { isSpoofingToLessThan("19.00.00"); private static final boolean SHORTS_CUSTOM_ACTIONS_FLYOUT_MENU_ENABLED = !IS_SPOOFING_TO_YOUTUBE_2023 && Settings.ENABLE_SHORTS_CUSTOM_ACTIONS_FLYOUT_MENU.get(); + private static final boolean SHORTS_CUSTOM_ACTIONS_TOOLBAR_ENABLED = + Settings.ENABLE_SHORTS_CUSTOM_ACTIONS_TOOLBAR.get(); private static final int arrSize = CustomAction.values().length; private static final Map flyoutMenuMap = new LinkedHashMap<>(arrSize); private static WeakReference contextRef = new WeakReference<>(null); private static WeakReference recyclerViewRef = new WeakReference<>(null); + + /** + * Injection point. + */ + public static void setToolbarMenu(String enumString, View toolbarView) { + if (!SHORTS_CUSTOM_ACTIONS_TOOLBAR_ENABLED) { + return; + } + if (ShortsPlayerState.getCurrent().isClosed()) { + return; + } + if (!isMoreButton(enumString)) { + return; + } + setToolbarMenuOnLongClickListener((ViewGroup) toolbarView); + } + + private static void setToolbarMenuOnLongClickListener(ViewGroup parentView) { + ImageView imageView = Utils.getChildView(parentView, v -> v instanceof ImageView); + if (imageView == null) { + return; + } + Context context = imageView.getContext(); + contextRef = new WeakReference<>(context); + + // Overriding is possible only after OnClickListener is assigned to the more button. + Utils.runOnMainThreadDelayed(() -> imageView.setOnLongClickListener(button -> { + showMoreButtonDialog(context); + return true; + }), 0); + } + + private static void showMoreButtonDialog(Context context) { + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(getString("revanced_shorts_custom_actions_toolbar_dialog_title")); + + Map toolbarMap = new LinkedHashMap<>(arrSize); + + for (CustomAction customAction : CustomAction.values()) { + if (customAction.settings.get()) { + toolbarMap.putIfAbsent(customAction.getLabel(), customAction.getOnClickAction()); + } + } + + String[] titles = toolbarMap.keySet().toArray(new String[0]); + Runnable[] actions = toolbarMap.values().toArray(new Runnable[0]); + builder.setItems(titles, (dialog, which) -> { + String selectedOption = titles[which]; + Runnable action = actions[which]; + if (action != null) { + action.run(); + } else { + Logger.printDebug(() -> "No action found for " + selectedOption); + } + }); + + + AlertDialog dialog = builder.create(); + dialog.show(); + } + + private static boolean isMoreButton(String enumString) { + return StringUtils.equalsAny( + enumString, + "MORE_VERT", + "MORE_VERT_BOLD" + ); + } + /** * Injection point. */ @@ -129,6 +204,24 @@ public final class CustomActionsPatch { }); } + /** + * Injection point. + */ + public static void onLiveHeaderElementsContainerCreate(final View view) { + if (!SHORTS_CUSTOM_ACTIONS_TOOLBAR_ENABLED) { + return; + } + view.getViewTreeObserver().addOnDrawListener(() -> { + try { + if (view instanceof ViewGroup viewGroup) { + setToolbarMenuOnLongClickListener(viewGroup); + } + } catch (Exception ex) { + Logger.printException(() -> "onFlyoutMenuCreate failure", ex); + } + }); + } + private static void hideFlyoutMenu() { if (!SHORTS_CUSTOM_ACTIONS_FLYOUT_MENU_ENABLED) { return; diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/Settings.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/Settings.java index c76ab9a15..ab4683227 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/Settings.java +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/Settings.java @@ -471,16 +471,17 @@ public class Settings extends BaseSettings { // PreferenceScreen: Shorts - Shorts player components - Custom actions public static final BooleanSetting ENABLE_SHORTS_CUSTOM_ACTIONS_FLYOUT_MENU = new BooleanSetting("revanced_enable_shorts_custom_actions_flyout_menu", FALSE, true); + public static final BooleanSetting ENABLE_SHORTS_CUSTOM_ACTIONS_TOOLBAR = new BooleanSetting("revanced_enable_shorts_custom_actions_toolbar", FALSE, true); public static final BooleanSetting SHORTS_CUSTOM_ACTIONS_COPY_VIDEO_URL = new BooleanSetting("revanced_shorts_custom_actions_copy_video_url", FALSE, true, - parentsAny(ENABLE_SHORTS_CUSTOM_ACTIONS_FLYOUT_MENU)); + parentsAny(ENABLE_SHORTS_CUSTOM_ACTIONS_FLYOUT_MENU, ENABLE_SHORTS_CUSTOM_ACTIONS_TOOLBAR)); public static final BooleanSetting SHORTS_CUSTOM_ACTIONS_COPY_VIDEO_URL_TIMESTAMP = new BooleanSetting("revanced_shorts_custom_actions_copy_video_url_timestamp", FALSE, true, - parentsAny(ENABLE_SHORTS_CUSTOM_ACTIONS_FLYOUT_MENU)); + parentsAny(ENABLE_SHORTS_CUSTOM_ACTIONS_FLYOUT_MENU, ENABLE_SHORTS_CUSTOM_ACTIONS_TOOLBAR)); public static final BooleanSetting SHORTS_CUSTOM_ACTIONS_EXTERNAL_DOWNLOADER = new BooleanSetting("revanced_shorts_custom_actions_external_downloader", FALSE, true, - parentsAny(ENABLE_SHORTS_CUSTOM_ACTIONS_FLYOUT_MENU)); + parentsAny(ENABLE_SHORTS_CUSTOM_ACTIONS_FLYOUT_MENU, ENABLE_SHORTS_CUSTOM_ACTIONS_TOOLBAR)); public static final BooleanSetting SHORTS_CUSTOM_ACTIONS_OPEN_VIDEO = new BooleanSetting("revanced_shorts_custom_actions_open_video", FALSE, true, - parentsAny(ENABLE_SHORTS_CUSTOM_ACTIONS_FLYOUT_MENU)); + parentsAny(ENABLE_SHORTS_CUSTOM_ACTIONS_FLYOUT_MENU, ENABLE_SHORTS_CUSTOM_ACTIONS_TOOLBAR)); public static final BooleanSetting SHORTS_CUSTOM_ACTIONS_REPEAT_STATE = new BooleanSetting("revanced_shorts_custom_actions_repeat_state", FALSE, true, - parentsAny(ENABLE_SHORTS_CUSTOM_ACTIONS_FLYOUT_MENU)); + parentsAny(ENABLE_SHORTS_CUSTOM_ACTIONS_FLYOUT_MENU, ENABLE_SHORTS_CUSTOM_ACTIONS_TOOLBAR)); // Experimental Flags public static final BooleanSetting ENABLE_TIME_STAMP = new BooleanSetting("revanced_enable_shorts_time_stamp", FALSE, true); 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 831ab3913..7bbb3aa9e 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 @@ -14,9 +14,13 @@ import app.revanced.patches.youtube.utils.resourceid.reelRightLikeIcon import app.revanced.patches.youtube.utils.resourceid.reelVodTimeStampsContainer import app.revanced.patches.youtube.utils.resourceid.rightComment 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.Method +import com.android.tools.smali.dexlib2.iface.reference.MethodReference internal val bottomSheetMenuListBuilderFingerprint = legacyFingerprint( name = "bottomSheetMenuListBuilderFingerprint", @@ -32,6 +36,23 @@ internal val bottomSheetMenuListBuilderFingerprint = legacyFingerprint( strings = listOf("Bottom Sheet Menu is empty. No menu items were supported."), ) +internal val liveHeaderElementsContainerFingerprint = legacyFingerprint( + name = "liveHeaderElementsContainerFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("Landroid/view/ViewGroup;", "L"), + strings = listOf("Header container is null, header cannot be presented."), + customFingerprint = { method, _ -> + indexOfAddLiveHeaderElementsContainerInstruction(method) >= 0 + }, +) + +fun indexOfAddLiveHeaderElementsContainerInstruction(method: Method) = + method.indexOfFirstInstruction { + opcode == Opcode.INVOKE_DIRECT && + getReference()?.name == "addView" + } + internal val reelEnumConstructorFingerprint = legacyFingerprint( name = "reelEnumConstructorFingerprint", returnType = "V", 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 5a7ef0ab6..7f91f695e 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 @@ -29,7 +29,9 @@ import app.revanced.patches.youtube.utils.navigation.navigationBarHookPatch import app.revanced.patches.youtube.utils.patch.PatchList.SHORTS_COMPONENTS import app.revanced.patches.youtube.utils.playertype.playerTypeHookPatch import app.revanced.patches.youtube.utils.playservice.is_18_31_or_greater +import app.revanced.patches.youtube.utils.playservice.is_18_34_or_greater import app.revanced.patches.youtube.utils.playservice.is_18_49_or_greater +import app.revanced.patches.youtube.utils.playservice.is_19_02_or_greater import app.revanced.patches.youtube.utils.playservice.is_19_25_or_greater import app.revanced.patches.youtube.utils.playservice.is_19_28_or_greater import app.revanced.patches.youtube.utils.playservice.versionCheckPatch @@ -53,6 +55,8 @@ import app.revanced.patches.youtube.utils.resourceid.sharedResourceIdPatch import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference import app.revanced.patches.youtube.utils.settings.ResourceUtils.getContext import app.revanced.patches.youtube.utils.settings.settingsPatch +import app.revanced.patches.youtube.utils.toolbar.hookToolBar +import app.revanced.patches.youtube.utils.toolbar.toolBarHookPatch import app.revanced.patches.youtube.video.information.hookShortsVideoInformation import app.revanced.patches.youtube.video.information.videoInformationPatch import app.revanced.patches.youtube.video.videoid.hookPlayerResponseVideoId @@ -81,6 +85,7 @@ import app.revanced.util.or import app.revanced.util.replaceLiteralInstructionCall import com.android.tools.smali.dexlib2.AccessFlags 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.RegisterRangeInstruction @@ -153,16 +158,50 @@ private val shortsCustomActionsPatch = bytecodePatch( bottomSheetRecyclerViewPatch, lithoFilterPatch, playerTypeHookPatch, + toolBarHookPatch, videoIdPatch, videoInformationPatch, versionCheckPatch, ) execute { - if (!is_18_49_or_greater) { + if (!is_18_34_or_greater) { return@execute } + // region hook toolbar more button + + hookToolBar("$EXTENSION_CUSTOM_ACTIONS_CLASS_DESCRIPTOR->setToolbarMenu") + + // toolbar in Shorts livestream + liveHeaderElementsContainerFingerprint.methodOrThrow().apply { + val addViewIndex = indexOfAddLiveHeaderElementsContainerInstruction(this) + val viewRegister = getInstruction(addViewIndex).registerD + + addInstruction( + addViewIndex + 1, + "invoke-static {v$viewRegister}, " + + "$EXTENSION_CUSTOM_ACTIONS_CLASS_DESCRIPTOR->onLiveHeaderElementsContainerCreate(Landroid/view/View;)V" + ) + } + + // endregion + + // region add litho filter + + hookPlayerResponseVideoId("$SHORTS_PLAYER_FLYOUT_MENU_FILTER_CLASS_DESCRIPTOR->newPlayerResponseVideoId(Ljava/lang/String;Z)V") + hookShortsVideoInformation("$SHORTS_PLAYER_FLYOUT_MENU_FILTER_CLASS_DESCRIPTOR->newShortsVideoStarted(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;JZ)V") + + addLithoFilter(SHORTS_PLAYER_FLYOUT_MENU_FILTER_CLASS_DESCRIPTOR) + + // endregion + + if (!is_19_02_or_greater) { + return@execute + } + + // region hook flyout menu + bottomSheetMenuListBuilderFingerprint.matchOrThrow().let { it.method.apply { val addListIndex = indexOfFirstInstructionOrThrow { @@ -280,10 +319,7 @@ private val shortsCustomActionsPatch = bytecodePatch( bottomSheetRecyclerViewHook("$EXTENSION_CUSTOM_ACTIONS_CLASS_DESCRIPTOR->onFlyoutMenuCreate(Landroid/support/v7/widget/RecyclerView;)V") - hookPlayerResponseVideoId("$SHORTS_PLAYER_FLYOUT_MENU_FILTER_CLASS_DESCRIPTOR->newPlayerResponseVideoId(Ljava/lang/String;Z)V") - hookShortsVideoInformation("$SHORTS_PLAYER_FLYOUT_MENU_FILTER_CLASS_DESCRIPTOR->newShortsVideoStarted(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;JZ)V") - - addLithoFilter(SHORTS_PLAYER_FLYOUT_MENU_FILTER_CLASS_DESCRIPTOR) + // endregion } } @@ -571,8 +607,13 @@ val shortsComponentPatch = bytecodePatch( settingArray += "SETTINGS: SHORTS_TIME_STAMP" } - if (is_18_49_or_greater) { - settingArray += "SETTINGS: SHORTS_CUSTOM_ACTIONS" + if (is_18_34_or_greater) { + settingArray += "SETTINGS: SHORTS_CUSTOM_ACTIONS_SHARED" + settingArray += "SETTINGS: SHORTS_CUSTOM_ACTIONS_TOOLBAR" + } + + if (is_19_02_or_greater) { + settingArray += "SETTINGS: SHORTS_CUSTOM_ACTIONS_FLYOUT_MENU" } // region patch for hide comments button (non-litho) 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 bc871b68b..322d0d7a6 100644 --- a/patches/src/main/resources/youtube/settings/host/values/strings.xml +++ b/patches/src/main/resources/youtube/settings/host/values/strings.xml @@ -1364,6 +1364,11 @@ Limitations: • Does not work if app version is spoofed to 18.49.37 or earlier. • Does not work with livestream." Custom actions are disabled in flyout menu. + Enable custom actions in toolbar + "Custom actions are enabled in toolbar. + +Press and hold the More button to show the Custom actions dialog." + Custom actions are disabled in toolbar. Custom actions Copy video URL Show copy video URL menu @@ -1385,6 +1390,10 @@ Limitations: Show repeat state menu Repeat state menu is shown. Repeat state menu is hidden. + About Custom actions + "This feature is still experimental, so there is no guarantee that it will work perfectly. + +Most bugs cannot be fixed due to client-side limitations, so use it only for testing purposes." Enable timestamps diff --git a/patches/src/main/resources/youtube/settings/xml/revanced_prefs.xml b/patches/src/main/resources/youtube/settings/xml/revanced_prefs.xml index 9de166ae6..9989a5cb7 100644 --- a/patches/src/main/resources/youtube/settings/xml/revanced_prefs.xml +++ b/patches/src/main/resources/youtube/settings/xml/revanced_prefs.xml @@ -582,16 +582,26 @@ SETTINGS: SHORTS_COMPONENTS --> - + + + + + + - SETTINGS: SHORTS_CUSTOM_ACTIONS --> +