diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/client/YouTubeAppClient.kt b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/client/YouTubeAppClient.kt index f2459659d..6bf63b27e 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/client/YouTubeAppClient.kt +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/client/YouTubeAppClient.kt @@ -2,6 +2,7 @@ package app.revanced.extension.shared.patches.client import android.os.Build import app.revanced.extension.shared.settings.BaseSettings +import app.revanced.extension.shared.utils.PackageUtils import org.apache.commons.lang3.ArrayUtils import java.util.Locale @@ -73,6 +74,15 @@ object YouTubeAppClient { iOSUserAgent(PACKAGE_NAME_IOS_UNPLUGGED, CLIENT_VERSION_IOS_UNPLUGGED) + // ANDROID + private const val PACKAGE_NAME_ANDROID = "com.google.android.youtube" + private val CLIENT_VERSION_ANDROID = PackageUtils.getAppVersionName() + private val USER_AGENT_ANDROID = androidUserAgent( + packageName = PACKAGE_NAME_ANDROID, + clientVersion = CLIENT_VERSION_ANDROID, + ) + + // ANDROID VR /** * Video not playable: Kids @@ -91,7 +101,7 @@ object YouTubeAppClient { * [the App Store page of the YouTube app](https://www.meta.com/en-us/experiences/2002317119880945/), * in the `Additional details` section. */ - private const val CLIENT_VERSION_ANDROID_VR = "1.61.48" + private const val CLIENT_VERSION_ANDROID_VR = "1.62.27" /** * The device machine id for the Meta Quest 3, used to get opus codec with the Android VR client. @@ -281,6 +291,14 @@ object YouTubeAppClient { */ val friendlyName: String ) { + ANDROID( + id = 3, + userAgent = USER_AGENT_ANDROID, + androidSdkVersion = Build.VERSION.SDK, + clientVersion = CLIENT_VERSION_ANDROID, + clientName = "ANDROID", + friendlyName = "Android" + ), ANDROID_VR( id = 28, deviceMake = DEVICE_MAKE_ANDROID_VR, diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/SpoofStreamingDataPatch.java b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/SpoofStreamingDataPatch.java index d6db51b90..aaab9e096 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/SpoofStreamingDataPatch.java +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/SpoofStreamingDataPatch.java @@ -62,43 +62,20 @@ public class SpoofStreamingDataPatch extends BlockRequestPatch { return false; } - /** - * Parameters causing playback issues. - */ - private static final String[] PATH_NO_VIDEO_ID = { - "ad_break", // This request fetches a list of times when ads can be displayed. - "get_drm_license", // Waiting for a paid video to start. - "heartbeat", // This request determines whether to pause playback when the user is AFK. - "refresh", // Waiting for a livestream to start. - }; - /** * Injection point. */ public static void fetchStreams(String url, Map requestHeaders) { if (SPOOF_STREAMING_DATA) { - try { - Uri uri = Uri.parse(url); - String path = uri.getPath(); - if (path == null || !path.contains("player")) { - return; - } - - if (Utils.containsAny(path, PATH_NO_VIDEO_ID)) { - Logger.printDebug(() -> "Ignoring path: " + path); - return; - } - - String id = uri.getQueryParameter("id"); - if (id == null) { - Logger.printException(() -> "Ignoring request with no id: " + url); - return; - } - - StreamingDataRequest.fetchRequest(id, requestHeaders, VISITOR_DATA, PO_TOKEN); - } catch (Exception ex) { - Logger.printException(() -> "fetchStreams failure", ex); + String id = Utils.getVideoIdFromRequest(url); + if (id == null) { + Logger.printException(() -> "Ignoring request with no id: " + url); + return; + } else if (id.isEmpty()) { + return; } + + StreamingDataRequest.fetchRequest(id, requestHeaders, VISITOR_DATA, PO_TOKEN); } } diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/requests/PlayerRoutes.kt b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/requests/PlayerRoutes.kt index fe005ca55..39ece7fd9 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/requests/PlayerRoutes.kt +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/requests/PlayerRoutes.kt @@ -44,6 +44,17 @@ object PlayerRoutes { "&alt=proto" ).compile() + @JvmField + val GET_VIDEO_ACTION_BUTTON: CompiledRoute = Route( + Route.Method.POST, + "next" + + "?prettyPrint=false" + + "&fields=contents.singleColumnWatchNextResults." + + "results.results.contents.slimVideoMetadataSectionRenderer." + + "contents.elementRenderer.newElement.type.componentType." + + "model.videoActionBarModel.buttons.buttonViewModel" + ).compile() + @JvmField val GET_VIDEO_DETAILS: CompiledRoute = Route( Route.Method.POST, diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/requests/StreamingDataRequest.kt b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/requests/StreamingDataRequest.kt index 88fc71da2..425ccfcb3 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/requests/StreamingDataRequest.kt +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/requests/StreamingDataRequest.kt @@ -110,20 +110,13 @@ class StreamingDataRequest private constructor( private const val HTTP_TIMEOUT_MILLISECONDS = 10 * 1000 /** - * Any arbitrarily large value, but must be at least twice [.HTTP_TIMEOUT_MILLISECONDS] + * Any arbitrarily large value, but must be at least twice [HTTP_TIMEOUT_MILLISECONDS] */ private const val MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000 @GuardedBy("itself") val cache: MutableMap = Collections.synchronizedMap( object : LinkedHashMap(100) { - /** - * Cache limit must be greater than the maximum number of videos open at once, - * which theoretically is more than 4 (3 Shorts + one regular minimized video). - * But instead use a much larger value, to handle if a video viewed a while ago - * is somehow still referenced. Each stream is a small array of Strings - * so memory usage is not a concern. - */ private val CACHE_LIMIT = 50 override fun removeEldestEntry(eldest: Map.Entry): Boolean { diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/utils/Utils.java b/extensions/shared/src/main/java/app/revanced/extension/shared/utils/Utils.java index 5ef12c4a8..33154e8c8 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/shared/utils/Utils.java +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/utils/Utils.java @@ -11,6 +11,7 @@ import android.content.res.Configuration; import android.content.res.Resources; import android.net.ConnectivityManager; import android.net.NetworkInfo; +import android.net.Uri; import android.os.Build; import android.os.Handler; import android.os.Looper; @@ -432,6 +433,34 @@ public class Utils { setEditTextDialogTheme(builder, false); } + /** + * No video id in these parameters. + */ + private static final String[] PATH_NO_VIDEO_ID = { + "ad_break", // This request fetches a list of times when ads can be displayed. + "get_drm_license", // Waiting for a paid video to start. + "heartbeat", // This request determines whether to pause playback when the user is AFK. + "refresh", // Waiting for a livestream to start. + }; + + @Nullable + public static String getVideoIdFromRequest(String url) { + try { + Uri uri = Uri.parse(url); + String path = uri.getPath(); + if (path != null && path.contains("player")) { + if (!containsAny(path, PATH_NO_VIDEO_ID)) { + return uri.getQueryParameter("id"); + } else { + Logger.printDebug(() -> "Ignoring path: " + path); + } + } + } catch (Exception ex) { + Logger.printException(() -> "getVideoIdFromRequest failure", ex); + } + return ""; + } + /** * If {@link Fragment} uses [Android library] rather than [AndroidX library], * the Dialog theme corresponding to [Android library] should be used. diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/ActionButtonsFilter.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/ActionButtonsFilter.java index 69386f21f..680846c13 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/ActionButtonsFilter.java +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/ActionButtonsFilter.java @@ -6,6 +6,7 @@ import app.revanced.extension.shared.patches.components.ByteArrayFilterGroup; import app.revanced.extension.shared.patches.components.ByteArrayFilterGroupList; import app.revanced.extension.shared.patches.components.Filter; import app.revanced.extension.shared.patches.components.StringFilterGroup; +import app.revanced.extension.shared.settings.BooleanSetting; import app.revanced.extension.youtube.settings.Settings; @SuppressWarnings("unused") @@ -18,6 +19,8 @@ public final class ActionButtonsFilter extends Filter { private final StringFilterGroup likeSubscribeGlow; private final ByteArrayFilterGroupList bufferButtonsGroupList = new ByteArrayFilterGroupList(); + private static final boolean HIDE_ACTION_BUTTON_INDEX = Settings.HIDE_ACTION_BUTTON_INDEX.get(); + public ActionButtonsFilter() { actionBarRule = new StringFilterGroup( null, @@ -95,6 +98,9 @@ public final class ActionButtonsFilter extends Filter { @Override public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray, StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { + if (HIDE_ACTION_BUTTON_INDEX) { + return false; + } if (!path.startsWith(VIDEO_ACTION_BAR_PATH_PREFIX)) { return false; } diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/player/ActionButtonsPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/player/ActionButtonsPatch.java index 9cd6f6a0c..f3207ae8f 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/player/ActionButtonsPatch.java +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/player/ActionButtonsPatch.java @@ -2,53 +2,144 @@ package app.revanced.extension.youtube.patches.player; import androidx.annotation.Nullable; +import org.apache.commons.lang3.ArrayUtils; + +import java.util.Arrays; import java.util.List; +import java.util.Map; + +import static app.revanced.extension.youtube.patches.player.ActionButtonsPatch.ActionButton.*; import app.revanced.extension.shared.settings.BooleanSetting; import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.shared.utils.Utils; +import app.revanced.extension.youtube.patches.player.requests.ActionButtonRequest; import app.revanced.extension.youtube.settings.Settings; import app.revanced.extension.youtube.shared.VideoInformation; -@SuppressWarnings("unused") +@SuppressWarnings({"unused", "deprecation"}) public class ActionButtonsPatch { public enum ActionButton { - INDEX_7(Settings.HIDE_ACTION_BUTTON_INDEX_7, Settings.HIDE_ACTION_BUTTON_INDEX_LIVE_7, 7), - INDEX_6(Settings.HIDE_ACTION_BUTTON_INDEX_6, Settings.HIDE_ACTION_BUTTON_INDEX_LIVE_6, 6), - INDEX_5(Settings.HIDE_ACTION_BUTTON_INDEX_5, Settings.HIDE_ACTION_BUTTON_INDEX_LIVE_5, 5), - INDEX_4(Settings.HIDE_ACTION_BUTTON_INDEX_4, Settings.HIDE_ACTION_BUTTON_INDEX_LIVE_4, 4), - INDEX_3(Settings.HIDE_ACTION_BUTTON_INDEX_3, Settings.HIDE_ACTION_BUTTON_INDEX_LIVE_3, 3), - INDEX_2(Settings.HIDE_ACTION_BUTTON_INDEX_2, Settings.HIDE_ACTION_BUTTON_INDEX_LIVE_2, 2), - INDEX_1(Settings.HIDE_ACTION_BUTTON_INDEX_1, Settings.HIDE_ACTION_BUTTON_INDEX_LIVE_1, 1), - INDEX_0(Settings.HIDE_ACTION_BUTTON_INDEX_0, Settings.HIDE_ACTION_BUTTON_INDEX_LIVE_0, 0); + UNKNOWN( + null, + null + ), + CLIP( + "clipButtonViewModel", + Settings.HIDE_CLIP_BUTTON + ), + DOWNLOAD( + "downloadButtonViewModel", + Settings.HIDE_DOWNLOAD_BUTTON + ), + LIKE_DISLIKE( + "segmentedLikeDislikeButtonViewModel", + Settings.HIDE_LIKE_DISLIKE_BUTTON + ), + LIVE_CHAT( + "yt_outline_message_bubble", + null + ), + PLAYLIST( + "addToPlaylistButtonViewModel", + Settings.HIDE_PLAYLIST_BUTTON + ), + REMIX( + "yt_outline_youtube_shorts_plus", + Settings.HIDE_REMIX_BUTTON + ), + REPORT( + "yt_outline_flag", + Settings.HIDE_REPORT_BUTTON + ), + REWARDS( + "yt_outline_account_link", + Settings.HIDE_REWARDS_BUTTON + ), + SHARE( + "yt_outline_share", + Settings.HIDE_SHARE_BUTTON + ), + SHOP( + "yt_outline_bag", + Settings.HIDE_SHOP_BUTTON + ), + THANKS( + "yt_outline_dollar_sign_heart", + Settings.HIDE_THANKS_BUTTON + ); - private final BooleanSetting generalSetting; - private final BooleanSetting liveSetting; - private final int index; + @Nullable + public final String identifier; + @Nullable + public final BooleanSetting setting; - ActionButton(final BooleanSetting generalSetting, final BooleanSetting liveSetting, final int index) { - this.generalSetting = generalSetting; - this.liveSetting = liveSetting; - this.index = index; + ActionButton(@Nullable String identifier, @Nullable BooleanSetting setting) { + this.identifier = identifier; + this.setting = setting; } } private static final String TARGET_COMPONENT_TYPE = "LazilyConvertedElement"; private static final String VIDEO_ACTION_BAR_PATH_PREFIX = "video_action_bar.eml"; + private static final boolean HIDE_ACTION_BUTTON_INDEX = Settings.HIDE_ACTION_BUTTON_INDEX.get(); + private static final int REMIX_INDEX = Settings.REMIX_BUTTON_INDEX.get() - 1; + /** + * Injection point. + */ + public static void fetchStreams(String url, Map requestHeaders) { + if (HIDE_ACTION_BUTTON_INDEX) { + String id = Utils.getVideoIdFromRequest(url); + if (id == null) { + Logger.printException(() -> "Ignoring request with no id: " + url); + return; + } else if (id.isEmpty()) { + return; + } + + ActionButtonRequest.fetchRequestIfNeeded(id, requestHeaders); + } + } + + /** + * Injection point. + * + * @param list Type list of litho components + * @param identifier Identifier of litho components + */ public static List hideActionButtonByIndex(@Nullable List list, @Nullable String identifier) { try { - if (identifier != null && + if (HIDE_ACTION_BUTTON_INDEX && + identifier != null && identifier.startsWith(VIDEO_ACTION_BAR_PATH_PREFIX) && list != null && !list.isEmpty() && list.get(0).toString().equals(TARGET_COMPONENT_TYPE) ) { - final int size = list.size(); - final boolean isLive = VideoInformation.getLiveStreamState(); - for (ActionButton button : ActionButton.values()) { - if (size > button.index && (isLive ? button.liveSetting.get() : button.generalSetting.get())) { - list.remove(button.index); + final int listSize = list.size(); + final String videoId = VideoInformation.getVideoId(); + ActionButtonRequest request = ActionButtonRequest.getRequestForVideoId(videoId); + if (request != null) { + ActionButton[] actionButtons = request.getArray(); + final int actionButtonsLength = actionButtons.length; + // The response is always included with the [LIKE_DISLIKE] button and the [SHARE] button. + // The minimum size of the action button array is 3. + if (actionButtonsLength > 2) { + // For some reason, the response does not contain the [REMIX] button. + // Add the [REMIX] button manually. + if (listSize - actionButtonsLength == 1) { + actionButtons = ArrayUtils.add(actionButtons, REMIX_INDEX, REMIX); + } + ActionButton[] finalActionButtons = actionButtons; + Logger.printDebug(() -> "videoId: " + videoId + ", buttons: " + Arrays.toString(finalActionButtons)); + for (int i = actionButtons.length - 1; i > -1; i--) { + ActionButton actionButton = actionButtons[i]; + if (actionButton.setting != null && actionButton.setting.get()) { + list.remove(i); + } + } } } } diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/player/requests/ActionButtonRequest.kt b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/player/requests/ActionButtonRequest.kt new file mode 100644 index 000000000..a10bd26d4 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/player/requests/ActionButtonRequest.kt @@ -0,0 +1,226 @@ +package app.revanced.extension.youtube.patches.player.requests + +import androidx.annotation.GuardedBy +import app.revanced.extension.shared.patches.client.YouTubeAppClient +import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes +import app.revanced.extension.shared.requests.Requester +import app.revanced.extension.shared.utils.Logger +import app.revanced.extension.shared.utils.Utils +import app.revanced.extension.youtube.patches.player.ActionButtonsPatch.ActionButton +import org.json.JSONException +import org.json.JSONObject +import java.io.IOException +import java.net.SocketTimeoutException +import java.util.Collections +import java.util.Objects +import java.util.concurrent.ExecutionException +import java.util.concurrent.Future +import java.util.concurrent.TimeUnit +import java.util.concurrent.TimeoutException + +class ActionButtonRequest private constructor( + private val videoId: String, + private val playerHeaders: Map, +) { + private val future: Future> = Utils.submitOnBackgroundThread { + fetch(videoId, playerHeaders) + } + + val array: Array + get() { + try { + return future[MAX_MILLISECONDS_TO_WAIT_FOR_FETCH.toLong(), TimeUnit.MILLISECONDS] + } catch (ex: TimeoutException) { + Logger.printInfo( + { "getArray timed out" }, + ex + ) + } catch (ex: InterruptedException) { + Logger.printException( + { "getArray interrupted" }, + ex + ) + Thread.currentThread().interrupt() // Restore interrupt status flag. + } catch (ex: ExecutionException) { + Logger.printException( + { "getArray failure" }, + ex + ) + } + + return emptyArray() + } + + companion object { + /** + * TCP connection and HTTP read timeout. + */ + private const val HTTP_TIMEOUT_MILLISECONDS = 10 * 1000 + + /** + * Any arbitrarily large value, but must be at least twice [HTTP_TIMEOUT_MILLISECONDS] + */ + private const val MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000 + + @GuardedBy("itself") + val cache: MutableMap = Collections.synchronizedMap( + object : LinkedHashMap(100) { + private val CACHE_LIMIT = 50 + + override fun removeEldestEntry(eldest: Map.Entry): Boolean { + return size > CACHE_LIMIT // Evict the oldest entry if over the cache limit. + } + }) + + @JvmStatic + fun fetchRequestIfNeeded(videoId: String, playerHeaders: Map) { + Objects.requireNonNull(videoId) + synchronized(cache) { + if (!cache.containsKey(videoId)) { + cache[videoId] = ActionButtonRequest(videoId, playerHeaders) + } + } + } + + @JvmStatic + fun getRequestForVideoId(videoId: String): ActionButtonRequest? { + synchronized(cache) { + return cache[videoId] + } + } + + private fun handleConnectionError(toastMessage: String, ex: Exception?) { + Logger.printInfo({ toastMessage }, ex) + } + + private val REQUEST_HEADER_KEYS = arrayOf( + "Authorization", // Available only to logged-in users. + "X-GOOG-API-FORMAT-VERSION", + "X-Goog-Visitor-Id" + ) + + private fun sendRequest(videoId: String, playerHeaders: Map): JSONObject? { + Objects.requireNonNull(videoId) + + val startTime = System.currentTimeMillis() + // '/next' request does not require PoToken. + val clientType = YouTubeAppClient.ClientType.ANDROID + val clientTypeName = clientType.name + Logger.printDebug { "Fetching playlist request for: $videoId, using client: $clientTypeName" } + + try { + val connection = PlayerRoutes.getPlayerResponseConnectionFromRoute( + PlayerRoutes.GET_VIDEO_ACTION_BUTTON, + clientType + ) + connection.connectTimeout = HTTP_TIMEOUT_MILLISECONDS + connection.readTimeout = HTTP_TIMEOUT_MILLISECONDS + + // Since [THANKS] button and [CLIP] button are shown only with the logged in, + // Set the [Authorization] field to property to get the correct action buttons. + for (key in REQUEST_HEADER_KEYS) { + var value = playerHeaders[key] + if (value != null) { + connection.setRequestProperty(key, value) + } + } + + val requestBody = + PlayerRoutes.createApplicationRequestBody( + clientType = clientType, + videoId = videoId + ) + + connection.setFixedLengthStreamingMode(requestBody.size) + connection.outputStream.write(requestBody) + + val responseCode = connection.responseCode + if (responseCode == 200) return Requester.parseJSONObject(connection) + + handleConnectionError( + (clientTypeName + " not available with response code: " + + responseCode + " message: " + connection.responseMessage), + null + ) + } catch (ex: SocketTimeoutException) { + handleConnectionError("Connection timeout", ex) + } catch (ex: IOException) { + handleConnectionError("Network error", ex) + } catch (ex: Exception) { + Logger.printException({ "sendApplicationRequest failed" }, ex) + } finally { + Logger.printDebug { "video: " + videoId + " took: " + (System.currentTimeMillis() - startTime) + "ms" } + } + + return null + } + + private fun parseResponse(json: JSONObject): Array { + try { + val secondaryContentsJsonObject = + json.getJSONObject("contents") + .getJSONObject("singleColumnWatchNextResults") + .getJSONObject("results") + .getJSONObject("results") + .getJSONArray("contents") + .get(0) + + if (secondaryContentsJsonObject is JSONObject) { + val tertiaryContentsJsonArray = + secondaryContentsJsonObject + .getJSONObject("slimVideoMetadataSectionRenderer") + .getJSONArray("contents") + + val elementRendererJsonObject = + tertiaryContentsJsonArray + .get(tertiaryContentsJsonArray.length() - 1) + + if (elementRendererJsonObject is JSONObject) { + val buttons = + elementRendererJsonObject + .getJSONObject("elementRenderer") + .getJSONObject("newElement") + .getJSONObject("type") + .getJSONObject("componentType") + .getJSONObject("model") + .getJSONObject("videoActionBarModel") + .getJSONArray("buttons") + + val length = buttons.length() + val buttonsArr = Array(length) { ActionButton.UNKNOWN } + + for (i in 0 until length) { + val jsonObjectString = buttons.get(i).toString() + for (b in ActionButton.entries) { + if (b.identifier != null && jsonObjectString.contains(b.identifier)) { + buttonsArr[i] = b + } + } + } + + // Still, the response includes the [LIVE_CHAT] button. + // In the Android YouTube client, this button moved to the comments. + return buttonsArr.filter { it.setting != null }.toTypedArray() + } + } + } catch (e: JSONException) { + val jsonForMessage = json.toString().substring(3000) + Logger.printException( + { "Fetch failed while processing response data for response: $jsonForMessage" }, + e + ) + } + + return emptyArray() + } + + private fun fetch(videoId: String, playerHeaders: Map): Array { + val json = sendRequest(videoId, playerHeaders) + if (json != null) { + return parseResponse(json) + } + + return emptyArray() + } + } +} 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 a13257adf..5362c1f1d 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 @@ -291,22 +291,8 @@ public class Settings extends BaseSettings { public static final BooleanSetting HIDE_SHOP_BUTTON = new BooleanSetting("revanced_hide_shop_button", FALSE); public static final BooleanSetting HIDE_THANKS_BUTTON = new BooleanSetting("revanced_hide_thanks_button", FALSE); - public static final BooleanSetting HIDE_ACTION_BUTTON_INDEX_0 = new BooleanSetting("revanced_hide_action_button_index_0", FALSE); - public static final BooleanSetting HIDE_ACTION_BUTTON_INDEX_1 = new BooleanSetting("revanced_hide_action_button_index_1", FALSE); - public static final BooleanSetting HIDE_ACTION_BUTTON_INDEX_2 = new BooleanSetting("revanced_hide_action_button_index_2", FALSE); - public static final BooleanSetting HIDE_ACTION_BUTTON_INDEX_3 = new BooleanSetting("revanced_hide_action_button_index_3", FALSE); - public static final BooleanSetting HIDE_ACTION_BUTTON_INDEX_4 = new BooleanSetting("revanced_hide_action_button_index_4", FALSE); - public static final BooleanSetting HIDE_ACTION_BUTTON_INDEX_5 = new BooleanSetting("revanced_hide_action_button_index_5", FALSE); - public static final BooleanSetting HIDE_ACTION_BUTTON_INDEX_6 = new BooleanSetting("revanced_hide_action_button_index_6", FALSE); - public static final BooleanSetting HIDE_ACTION_BUTTON_INDEX_7 = new BooleanSetting("revanced_hide_action_button_index_7", FALSE); - public static final BooleanSetting HIDE_ACTION_BUTTON_INDEX_LIVE_0 = new BooleanSetting("revanced_hide_action_button_index_live_0", FALSE); - public static final BooleanSetting HIDE_ACTION_BUTTON_INDEX_LIVE_1 = new BooleanSetting("revanced_hide_action_button_index_live_1", FALSE); - public static final BooleanSetting HIDE_ACTION_BUTTON_INDEX_LIVE_2 = new BooleanSetting("revanced_hide_action_button_index_live_2", FALSE); - public static final BooleanSetting HIDE_ACTION_BUTTON_INDEX_LIVE_3 = new BooleanSetting("revanced_hide_action_button_index_live_3", FALSE); - public static final BooleanSetting HIDE_ACTION_BUTTON_INDEX_LIVE_4 = new BooleanSetting("revanced_hide_action_button_index_live_4", FALSE); - public static final BooleanSetting HIDE_ACTION_BUTTON_INDEX_LIVE_5 = new BooleanSetting("revanced_hide_action_button_index_live_5", FALSE); - public static final BooleanSetting HIDE_ACTION_BUTTON_INDEX_LIVE_6 = new BooleanSetting("revanced_hide_action_button_index_live_6", FALSE); - public static final BooleanSetting HIDE_ACTION_BUTTON_INDEX_LIVE_7 = new BooleanSetting("revanced_hide_action_button_index_live_7", FALSE); + public static final BooleanSetting HIDE_ACTION_BUTTON_INDEX = new BooleanSetting("revanced_hide_action_button_index", FALSE, true); + public static final IntegerSetting REMIX_BUTTON_INDEX = new IntegerSetting("revanced_remix_button_index", 3, true, parent(HIDE_ACTION_BUTTON_INDEX)); // PreferenceScreen: Player - Ambient mode public static final BooleanSetting BYPASS_AMBIENT_MODE_RESTRICTIONS = new BooleanSetting("revanced_bypass_ambient_mode_restrictions", FALSE); diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/player/action/ActionButtonsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/player/action/ActionButtonsPatch.kt index d1d695fdd..ce57443ec 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/player/action/ActionButtonsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/player/action/ActionButtonsPatch.kt @@ -1,7 +1,6 @@ package app.revanced.patches.youtube.player.action import app.revanced.patcher.extensions.InstructionExtensions.getInstruction -import app.revanced.patcher.patch.booleanOption import app.revanced.patcher.patch.bytecodePatch import app.revanced.patches.shared.litho.addLithoFilter import app.revanced.patches.shared.litho.emptyComponentsFingerprint @@ -10,10 +9,11 @@ import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PAC import app.revanced.patches.youtube.utils.extension.Constants.COMPONENTS_PATH import app.revanced.patches.youtube.utils.extension.Constants.PLAYER_PATH import app.revanced.patches.youtube.utils.patch.PatchList.HIDE_ACTION_BUTTONS +import app.revanced.patches.youtube.utils.request.buildRequestPatch +import app.revanced.patches.youtube.utils.request.hookBuildRequest import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference import app.revanced.patches.youtube.utils.settings.settingsPatch import app.revanced.patches.youtube.video.information.videoInformationPatch -import app.revanced.util.Utils.trimIndentMultiline import app.revanced.util.addInstructionsAtControlFlowLabel import app.revanced.util.findMethodOrThrow import app.revanced.util.fingerprint.methodOrThrow @@ -44,72 +44,61 @@ val actionButtonsPatch = bytecodePatch( settingsPatch, lithoFilterPatch, videoInformationPatch, - ) - - val hideActionButtonByIndex by booleanOption( - key = "hideActionButtonByIndex", - default = false, - title = "Hide action buttons by index", - description = """ - Add an option to hide action buttons by index. - - This setting is still experimental, so use it only for debugging purposes. - """.trimIndentMultiline(), - required = true + buildRequestPatch, ) execute { addLithoFilter(FILTER_CLASS_DESCRIPTOR) - var settingArray = arrayOf( - "PREFERENCE_SCREEN: PLAYER", - "SETTINGS: HIDE_ACTION_BUTTONS" - ) + // region patch for hide action buttons by index - if (hideActionButtonByIndex == true) { - componentListFingerprint.methodOrThrow(emptyComponentsFingerprint).apply { - val conversionContextToStringMethod = - findMethodOrThrow(parameters[1].type) { - name == "toString" - } - val identifierReference = with (conversionContextToStringMethod) { - val identifierStringIndex = - indexOfFirstStringInstructionOrThrow(", identifierProperty=") - val identifierStringAppendIndex = - indexOfFirstInstructionOrThrow(identifierStringIndex, Opcode.INVOKE_VIRTUAL) - val identifierStringAppendIndexRegister = getInstruction(identifierStringAppendIndex).registerD - val identifierAppendIndex = - indexOfFirstInstructionOrThrow(identifierStringAppendIndex + 1, Opcode.INVOKE_VIRTUAL) - val identifierRegister = getInstruction(identifierAppendIndex).registerD - val identifierIndex = indexOfFirstInstructionReversedOrThrow(identifierAppendIndex) { - opcode == Opcode.IGET_OBJECT && - getReference()?.type == "Ljava/lang/String;" && - (this as? TwoRegisterInstruction)?.registerA == identifierRegister - } - getInstruction(identifierIndex).reference + componentListFingerprint.methodOrThrow(emptyComponentsFingerprint).apply { + val conversionContextToStringMethod = + findMethodOrThrow(parameters[1].type) { + name == "toString" } - - val listIndex = implementation!!.instructions.lastIndex - val listRegister = getInstruction(listIndex).registerA - val identifierRegister = listRegister + 1 - - addInstructionsAtControlFlowLabel( - listIndex, """ - move-object/from16 v$identifierRegister, p2 - iget-object v$identifierRegister, v$identifierRegister, $identifierReference - invoke-static {v$listRegister, v$identifierRegister}, $ACTION_BUTTONS_CLASS_DESCRIPTOR->hideActionButtonByIndex(Ljava/util/List;Ljava/lang/String;)Ljava/util/List; - move-result-object v$listRegister - """ - ) - - settingArray += "SETTINGS: HIDE_BUTTONS_BY_INDEX" + val identifierReference = with (conversionContextToStringMethod) { + val identifierStringIndex = + indexOfFirstStringInstructionOrThrow(", identifierProperty=") + val identifierStringAppendIndex = + indexOfFirstInstructionOrThrow(identifierStringIndex, Opcode.INVOKE_VIRTUAL) + val identifierStringAppendIndexRegister = getInstruction(identifierStringAppendIndex).registerD + val identifierAppendIndex = + indexOfFirstInstructionOrThrow(identifierStringAppendIndex + 1, Opcode.INVOKE_VIRTUAL) + val identifierRegister = getInstruction(identifierAppendIndex).registerD + val identifierIndex = indexOfFirstInstructionReversedOrThrow(identifierAppendIndex) { + opcode == Opcode.IGET_OBJECT && + getReference()?.type == "Ljava/lang/String;" && + (this as? TwoRegisterInstruction)?.registerA == identifierRegister + } + getInstruction(identifierIndex).reference } + + val listIndex = implementation!!.instructions.lastIndex + val listRegister = getInstruction(listIndex).registerA + val identifierRegister = listRegister + 1 + + addInstructionsAtControlFlowLabel( + listIndex, """ + move-object/from16 v$identifierRegister, p2 + iget-object v$identifierRegister, v$identifierRegister, $identifierReference + invoke-static {v$listRegister, v$identifierRegister}, $ACTION_BUTTONS_CLASS_DESCRIPTOR->hideActionButtonByIndex(Ljava/util/List;Ljava/lang/String;)Ljava/util/List; + move-result-object v$listRegister + """ + ) } + hookBuildRequest("$ACTION_BUTTONS_CLASS_DESCRIPTOR->fetchStreams(Ljava/lang/String;Ljava/util/Map;)V") + + // endregion + // region add settings addPreference( - settingArray, + arrayOf( + "PREFERENCE_SCREEN: PLAYER", + "SETTINGS: HIDE_ACTION_BUTTONS" + ), HIDE_ACTION_BUTTONS ) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/streamingdata/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/streamingdata/Fingerprints.kt index d7bf166aa..c5ff68e76 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/streamingdata/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/streamingdata/Fingerprints.kt @@ -33,38 +33,6 @@ internal val buildMediaDataSourceFingerprint = legacyFingerprint( ) ) -internal val buildRequestFingerprint = legacyFingerprint( - name = "buildRequestFingerprint", - customFingerprint = { method, _ -> - method.implementation != null && - indexOfRequestFinishedListenerInstruction(method) >= 0 && - !method.definingClass.startsWith("Lorg/") && - indexOfNewUrlRequestBuilderInstruction(method) >= 0 && - // Earlier targets - (indexOfEntrySetInstruction(method) >= 0 || - // Later targets - method.parameters[1].type == "Ljava/util/Map;") - } -) - -internal fun indexOfRequestFinishedListenerInstruction(method: Method) = - method.indexOfFirstInstruction { - opcode == Opcode.INVOKE_VIRTUAL && - getReference()?.name == "setRequestFinishedListener" - } - -internal fun indexOfNewUrlRequestBuilderInstruction(method: Method) = - method.indexOfFirstInstruction { - opcode == Opcode.INVOKE_VIRTUAL && - getReference().toString() == "Lorg/chromium/net/CronetEngine;->newUrlRequestBuilder(Ljava/lang/String;Lorg/chromium/net/UrlRequest${'$'}Callback;Ljava/util/concurrent/Executor;)Lorg/chromium/net/UrlRequest${'$'}Builder;" - } - -internal fun indexOfEntrySetInstruction(method: Method) = - method.indexOfFirstInstruction { - opcode == Opcode.INVOKE_INTERFACE && - getReference().toString() == "Ljava/util/Map;->entrySet()Ljava/util/Set;" - } - internal val createStreamingDataFingerprint = legacyFingerprint( name = "createStreamingDataFingerprint", accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR, diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/streamingdata/SpoofStreamingDataPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/streamingdata/SpoofStreamingDataPatch.kt index fbb9151f0..8954d9933 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/streamingdata/SpoofStreamingDataPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/streamingdata/SpoofStreamingDataPatch.kt @@ -18,6 +18,8 @@ import app.revanced.patches.shared.spoof.useragent.baseSpoofUserAgentPatch import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE import app.revanced.patches.youtube.utils.compatibility.Constants.YOUTUBE_PACKAGE_NAME import app.revanced.patches.youtube.utils.patch.PatchList.SPOOF_STREAMING_DATA +import app.revanced.patches.youtube.utils.request.buildRequestPatch +import app.revanced.patches.youtube.utils.request.hookBuildRequest import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference import app.revanced.patches.youtube.utils.settings.settingsPatch import app.revanced.util.findInstructionIndicesReversedOrThrow @@ -31,7 +33,6 @@ import app.revanced.util.indexOfFirstInstructionOrThrow import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation -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 @@ -39,7 +40,7 @@ import com.android.tools.smali.dexlib2.iface.reference.FieldReference import com.android.tools.smali.dexlib2.immutable.ImmutableMethod import com.android.tools.smali.dexlib2.immutable.ImmutableMethodParameter -const val EXTENSION_CLASS_DESCRIPTOR = +private const val EXTENSION_CLASS_DESCRIPTOR = "$SPOOF_PATH/SpoofStreamingDataPatch;" val spoofStreamingDataPatch = bytecodePatch( @@ -52,36 +53,14 @@ val spoofStreamingDataPatch = bytecodePatch( settingsPatch, baseSpoofUserAgentPatch(YOUTUBE_PACKAGE_NAME), blockRequestPatch, + buildRequestPatch, ) execute { // region Get replacement streams at player requests. - buildRequestFingerprint.methodOrThrow().apply { - val newRequestBuilderIndex = indexOfNewUrlRequestBuilderInstruction(this) - val urlRegister = - getInstruction(newRequestBuilderIndex).registerD - - val entrySetIndex = indexOfEntrySetInstruction(this) - val mapRegister = if (entrySetIndex < 0) - urlRegister + 1 - else - getInstruction(entrySetIndex).registerC - - var smaliInstructions = - "invoke-static { v$urlRegister, v$mapRegister }, " + - "$EXTENSION_CLASS_DESCRIPTOR->" + - "fetchStreams(Ljava/lang/String;Ljava/util/Map;)V" - - if (entrySetIndex < 0) smaliInstructions = """ - move-object/from16 v$mapRegister, p1 - - """ + smaliInstructions - - // Copy request headers for streaming data fetch. - addInstructions(newRequestBuilderIndex + 2, smaliInstructions) - } + hookBuildRequest("$EXTENSION_CLASS_DESCRIPTOR->fetchStreams(Ljava/lang/String;Ljava/util/Map;)V") // endregion diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/request/BuildRequestPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/request/BuildRequestPatch.kt new file mode 100644 index 000000000..6c1c600b9 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/request/BuildRequestPatch.kt @@ -0,0 +1,56 @@ +package app.revanced.patches.youtube.utils.request + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +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.extension.sharedExtensionPatch +import app.revanced.util.fingerprint.methodOrThrow +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction + +private lateinit var buildRequestMethod: MutableMethod +private var urlRegister = 0 +private var mapRegister = 0 +private var offSet = 0 + +val buildRequestPatch = bytecodePatch( + description = "buildRequestPatch", +) { + dependsOn(sharedExtensionPatch) + + execute { + buildRequestFingerprint.methodOrThrow().apply { + buildRequestMethod = this + + val newRequestBuilderIndex = indexOfNewUrlRequestBuilderInstruction(this) + urlRegister = + getInstruction(newRequestBuilderIndex).registerD + + val entrySetIndex = indexOfEntrySetInstruction(this) + val isLegacyTarget = entrySetIndex < 0 + mapRegister = if (isLegacyTarget) + urlRegister + 1 + else + getInstruction(entrySetIndex).registerC + + if (isLegacyTarget) { + addInstructions( + newRequestBuilderIndex + 2, + "move-object/from16 v$mapRegister, p1" + ) + offSet++ + } + } + } +} + +internal fun hookBuildRequest(descriptor: String) { + buildRequestMethod.apply { + val insertIndex = indexOfNewUrlRequestBuilderInstruction(this) + 2 + offSet + + addInstructions( + insertIndex, + "invoke-static { v$urlRegister, v$mapRegister }, $descriptor" + ) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/request/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/request/Fingerprints.kt new file mode 100644 index 000000000..5906be353 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/request/Fingerprints.kt @@ -0,0 +1,40 @@ +package app.revanced.patches.youtube.utils.request + +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstruction +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 buildRequestFingerprint = legacyFingerprint( + name = "buildRequestFingerprint", + customFingerprint = { method, _ -> + method.implementation != null && + indexOfRequestFinishedListenerInstruction(method) >= 0 && + !method.definingClass.startsWith("Lorg/") && + indexOfNewUrlRequestBuilderInstruction(method) >= 0 && + // Earlier targets + (indexOfEntrySetInstruction(method) >= 0 || + // Later targets + method.parameters[1].type == "Ljava/util/Map;") + } +) + +internal fun indexOfRequestFinishedListenerInstruction(method: Method) = + method.indexOfFirstInstruction { + opcode == Opcode.INVOKE_VIRTUAL && + getReference()?.name == "setRequestFinishedListener" + } + +internal fun indexOfNewUrlRequestBuilderInstruction(method: Method) = + method.indexOfFirstInstruction { + opcode == Opcode.INVOKE_VIRTUAL && + getReference().toString() == "Lorg/chromium/net/CronetEngine;->newUrlRequestBuilder(Ljava/lang/String;Lorg/chromium/net/UrlRequest${'$'}Callback;Ljava/util/concurrent/Executor;)Lorg/chromium/net/UrlRequest${'$'}Builder;" + } + +internal fun indexOfEntrySetInstruction(method: Method) = + method.indexOfFirstInstruction { + opcode == Opcode.INVOKE_INTERFACE && + getReference().toString() == "Ljava/util/Map;->entrySet()Ljava/util/Set;" + } \ No newline at end of file diff --git a/patches/src/main/resources/youtube/settings/host/values/arrays.xml b/patches/src/main/resources/youtube/settings/host/values/arrays.xml index aea3a5f9c..c3dd9f126 100644 --- a/patches/src/main/resources/youtube/settings/host/values/arrays.xml +++ b/patches/src/main/resources/youtube/settings/host/values/arrays.xml @@ -435,6 +435,22 @@ 4 5 + + 3 + 4 + 5 + 6 + 7 + 8 + + + 3 + 4 + 5 + 6 + 7 + 8 + @string/revanced_watch_history_type_entry_1 @string/revanced_watch_history_type_entry_2 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 a51ec0e10..f90ab802f 100644 --- a/patches/src/main/resources/youtube/settings/host/values/strings.xml +++ b/patches/src/main/resources/youtube/settings/host/values/strings.xml @@ -891,42 +891,19 @@ Settings → Autoplay / Playback → Autoplay next video" Thanks button is hidden. Thanks button is shown. - - Hide by index + + Hide action button by index + "Action buttons are hidden by index. - Hide first button - First button is hidden. - First button is shown. - Hide second button - Second button is hidden. - Second button is shown. - Hide third button - Third button is hidden. - Third button is shown. - Hide fourth button - Fourth button is hidden. - Fourth button is shown. - Hide fifth button - Fifth button is hidden. - Fifth button is shown. - Hide sixth button - Sixth button is hidden. - Sixth button is shown. - Hide seventh button - Seventh button is hidden. - Seventh button is shown. - Hide eighth button - Eighth button is hidden. - Eighth button is shown. +Info: +• Wrong action buttons may be hidden, or action buttons may not be hidden. +• Hiding action buttons leaves no empty space." + "Action buttons are hidden by identifier filter. - - Hide by index in live stream - - About Hide action button by index - "Hide the action buttons by index before the action buttons are initialized. - -- Hiding the action buttons leaves no empty space. -- Index of the action buttons may not always be the same button." +Info: +• Right action buttons are hidden. +• Hiding action buttons leaves empty space." + Remix button index Ambient mode 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 1d9b0bfec..26c29f0ca 100644 --- a/patches/src/main/resources/youtube/settings/xml/revanced_prefs.xml +++ b/patches/src/main/resources/youtube/settings/xml/revanced_prefs.xml @@ -345,30 +345,10 @@ - SETTINGS: HIDE_ACTION_BUTTONS --> - - - -