feat(YouTube - Hide action buttons): Add setting Hide action button by index, Remove patch option Hide action buttons by index

This commit is contained in:
inotia00 2025-02-07 18:33:08 +09:00
parent 4358a4739b
commit 89920480c7
17 changed files with 591 additions and 249 deletions

View File

@ -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,

View File

@ -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<String, String> 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);
}
}

View File

@ -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,

View File

@ -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<String, StreamingDataRequest> = Collections.synchronizedMap(
object : LinkedHashMap<String, StreamingDataRequest>(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<String, StreamingDataRequest>): Boolean {

View File

@ -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.

View File

@ -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;
}

View File

@ -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<String, String> 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<Object> hideActionButtonByIndex(@Nullable List<Object> 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);
}
}
}
}
}

View File

@ -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<String, String>,
) {
private val future: Future<Array<ActionButton>> = Utils.submitOnBackgroundThread {
fetch(videoId, playerHeaders)
}
val array: Array<ActionButton>
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<String, ActionButtonRequest> = Collections.synchronizedMap(
object : LinkedHashMap<String, ActionButtonRequest>(100) {
private val CACHE_LIMIT = 50
override fun removeEldestEntry(eldest: Map.Entry<String, ActionButtonRequest>): Boolean {
return size > CACHE_LIMIT // Evict the oldest entry if over the cache limit.
}
})
@JvmStatic
fun fetchRequestIfNeeded(videoId: String, playerHeaders: Map<String, String>) {
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<String, String>): 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<ActionButton> {
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<ActionButton>(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<String, String>): Array<ActionButton> {
val json = sendRequest(videoId, playerHeaders)
if (json != null) {
return parseResponse(json)
}
return emptyArray()
}
}
}

View File

@ -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);

View File

@ -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<FiveRegisterInstruction>(identifierStringAppendIndex).registerD
val identifierAppendIndex =
indexOfFirstInstructionOrThrow(identifierStringAppendIndex + 1, Opcode.INVOKE_VIRTUAL)
val identifierRegister = getInstruction<FiveRegisterInstruction>(identifierAppendIndex).registerD
val identifierIndex = indexOfFirstInstructionReversedOrThrow(identifierAppendIndex) {
opcode == Opcode.IGET_OBJECT &&
getReference<FieldReference>()?.type == "Ljava/lang/String;" &&
(this as? TwoRegisterInstruction)?.registerA == identifierRegister
}
getInstruction<ReferenceInstruction>(identifierIndex).reference
componentListFingerprint.methodOrThrow(emptyComponentsFingerprint).apply {
val conversionContextToStringMethod =
findMethodOrThrow(parameters[1].type) {
name == "toString"
}
val listIndex = implementation!!.instructions.lastIndex
val listRegister = getInstruction<OneRegisterInstruction>(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<FiveRegisterInstruction>(identifierStringAppendIndex).registerD
val identifierAppendIndex =
indexOfFirstInstructionOrThrow(identifierStringAppendIndex + 1, Opcode.INVOKE_VIRTUAL)
val identifierRegister = getInstruction<FiveRegisterInstruction>(identifierAppendIndex).registerD
val identifierIndex = indexOfFirstInstructionReversedOrThrow(identifierAppendIndex) {
opcode == Opcode.IGET_OBJECT &&
getReference<FieldReference>()?.type == "Ljava/lang/String;" &&
(this as? TwoRegisterInstruction)?.registerA == identifierRegister
}
getInstruction<ReferenceInstruction>(identifierIndex).reference
}
val listIndex = implementation!!.instructions.lastIndex
val listRegister = getInstruction<OneRegisterInstruction>(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
)

View File

@ -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<MethodReference>()?.name == "setRequestFinishedListener"
}
internal fun indexOfNewUrlRequestBuilderInstruction(method: Method) =
method.indexOfFirstInstruction {
opcode == Opcode.INVOKE_VIRTUAL &&
getReference<MethodReference>().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<MethodReference>().toString() == "Ljava/util/Map;->entrySet()Ljava/util/Set;"
}
internal val createStreamingDataFingerprint = legacyFingerprint(
name = "createStreamingDataFingerprint",
accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR,

View File

@ -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<FiveRegisterInstruction>(newRequestBuilderIndex).registerD
val entrySetIndex = indexOfEntrySetInstruction(this)
val mapRegister = if (entrySetIndex < 0)
urlRegister + 1
else
getInstruction<FiveRegisterInstruction>(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

View File

@ -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<FiveRegisterInstruction>(newRequestBuilderIndex).registerD
val entrySetIndex = indexOfEntrySetInstruction(this)
val isLegacyTarget = entrySetIndex < 0
mapRegister = if (isLegacyTarget)
urlRegister + 1
else
getInstruction<FiveRegisterInstruction>(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"
)
}
}

View File

@ -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<MethodReference>()?.name == "setRequestFinishedListener"
}
internal fun indexOfNewUrlRequestBuilderInstruction(method: Method) =
method.indexOfFirstInstruction {
opcode == Opcode.INVOKE_VIRTUAL &&
getReference<MethodReference>().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<MethodReference>().toString() == "Ljava/util/Map;->entrySet()Ljava/util/Set;"
}

View File

@ -435,6 +435,22 @@
<item>4</item>
<item>5</item>
</string-array>
<string-array name="revanced_remix_button_index_entries">
<item>3</item>
<item>4</item>
<item>5</item>
<item>6</item>
<item>7</item>
<item>8</item>
</string-array>
<string-array name="revanced_remix_button_index_entry_values">
<item>3</item>
<item>4</item>
<item>5</item>
<item>6</item>
<item>7</item>
<item>8</item>
</string-array>
<string-array name="revanced_watch_history_type_entries">
<item>@string/revanced_watch_history_type_entry_1</item>
<item>@string/revanced_watch_history_type_entry_2</item>

View File

@ -891,42 +891,19 @@ Settings → Autoplay / Playback → Autoplay next video"</string>
<string name="revanced_hide_thanks_button_summary_on">Thanks button is hidden.</string>
<string name="revanced_hide_thanks_button_summary_off">Thanks button is shown.</string>
<!-- PreferenceScreen: Player, PreferenceCategory: Player, PreferenceScreen: Action buttons, PreferenceCategory: Hide by index -->
<string name="revanced_preference_category_hide_by_index">Hide by index</string>
<!-- PreferenceScreen: Player, PreferenceCategory: Player, PreferenceScreen: Action buttons, PreferenceCategory: Experimental flags -->
<string name="revanced_hide_action_button_index_title">Hide action button by index</string>
<string name="revanced_hide_action_button_index_summary_on">"Action buttons are hidden by index.
<string name="revanced_hide_action_button_index_0_title">Hide first button</string>
<string name="revanced_hide_action_button_index_0_summary_on">First button is hidden.</string>
<string name="revanced_hide_action_button_index_0_summary_off">First button is shown.</string>
<string name="revanced_hide_action_button_index_1_title">Hide second button</string>
<string name="revanced_hide_action_button_index_1_summary_on">Second button is hidden.</string>
<string name="revanced_hide_action_button_index_1_summary_off">Second button is shown.</string>
<string name="revanced_hide_action_button_index_2_title">Hide third button</string>
<string name="revanced_hide_action_button_index_2_summary_on">Third button is hidden.</string>
<string name="revanced_hide_action_button_index_2_summary_off">Third button is shown.</string>
<string name="revanced_hide_action_button_index_3_title">Hide fourth button</string>
<string name="revanced_hide_action_button_index_3_summary_on">Fourth button is hidden.</string>
<string name="revanced_hide_action_button_index_3_summary_off">Fourth button is shown.</string>
<string name="revanced_hide_action_button_index_4_title">Hide fifth button</string>
<string name="revanced_hide_action_button_index_4_summary_on">Fifth button is hidden.</string>
<string name="revanced_hide_action_button_index_4_summary_off">Fifth button is shown.</string>
<string name="revanced_hide_action_button_index_5_title">Hide sixth button</string>
<string name="revanced_hide_action_button_index_5_summary_on">Sixth button is hidden.</string>
<string name="revanced_hide_action_button_index_5_summary_off">Sixth button is shown.</string>
<string name="revanced_hide_action_button_index_6_title">Hide seventh button</string>
<string name="revanced_hide_action_button_index_6_summary_on">Seventh button is hidden.</string>
<string name="revanced_hide_action_button_index_6_summary_off">Seventh button is shown.</string>
<string name="revanced_hide_action_button_index_7_title">Hide eighth button</string>
<string name="revanced_hide_action_button_index_7_summary_on">Eighth button is hidden.</string>
<string name="revanced_hide_action_button_index_7_summary_off">Eighth button is shown.</string>
Info:
• Wrong action buttons may be hidden, or action buttons may not be hidden.
• Hiding action buttons leaves no empty space."</string>
<string name="revanced_hide_action_button_index_summary_off">"Action buttons are hidden by identifier filter.
<!-- PreferenceScreen: Player, PreferenceCategory: Player, PreferenceScreen: Action buttons, PreferenceCategory: Hide by index in live stream -->
<string name="revanced_preference_category_hide_by_index_live">Hide by index in live stream</string>
<string name="revanced_hide_action_button_index_about_title">About Hide action button by index</string>
<string name="revanced_hide_action_button_index_about_summary">"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."</string>
Info:
• Right action buttons are hidden.
• Hiding action buttons leaves empty space."</string>
<string name="revanced_remix_button_index_title">Remix button index</string>
<!-- PreferenceScreen: Player, PreferenceCategory: Player, PreferenceScreen: Ambient mode -->
<string name="revanced_preference_screen_ambient_mode_title">Ambient mode</string>

View File

@ -345,30 +345,10 @@
<SwitchPreference android:title="@string/revanced_hide_playlist_button_title" android:key="revanced_hide_playlist_button" android:summaryOn="@string/revanced_hide_playlist_button_summary_on" android:summaryOff="@string/revanced_hide_playlist_button_summary_off" />
<SwitchPreference android:title="@string/revanced_hide_share_button_title" android:key="revanced_hide_share_button" android:summaryOn="@string/revanced_hide_share_button_summary_on" android:summaryOff="@string/revanced_hide_share_button_summary_off" />
<SwitchPreference android:title="@string/revanced_hide_shop_button_title" android:key="revanced_hide_shop_button" android:summaryOn="@string/revanced_hide_shop_button_summary_on" android:summaryOff="@string/revanced_hide_shop_button_summary_off" />
<SwitchPreference android:title="@string/revanced_hide_thanks_button_title" android:key="revanced_hide_thanks_button" android:summaryOn="@string/revanced_hide_thanks_button_summary_on" android:summaryOff="@string/revanced_hide_thanks_button_summary_off" />SETTINGS: HIDE_ACTION_BUTTONS -->
<!-- SETTINGS: HIDE_BUTTONS_BY_INDEX
<PreferenceCategory android:title="@string/revanced_preference_category_hide_by_index" android:layout="@layout/revanced_settings_preferences_category"/>
<SwitchPreference android:title="@string/revanced_hide_action_button_index_0_title" android:key="revanced_hide_action_button_index_0" android:summaryOn="@string/revanced_hide_action_button_index_0_summary_on" android:summaryOff="@string/revanced_hide_action_button_index_0_summary_off" />
<SwitchPreference android:title="@string/revanced_hide_action_button_index_1_title" android:key="revanced_hide_action_button_index_1" android:summaryOn="@string/revanced_hide_action_button_index_1_summary_on" android:summaryOff="@string/revanced_hide_action_button_index_1_summary_off" />
<SwitchPreference android:title="@string/revanced_hide_action_button_index_2_title" android:key="revanced_hide_action_button_index_2" android:summaryOn="@string/revanced_hide_action_button_index_2_summary_on" android:summaryOff="@string/revanced_hide_action_button_index_2_summary_off" />
<SwitchPreference android:title="@string/revanced_hide_action_button_index_3_title" android:key="revanced_hide_action_button_index_3" android:summaryOn="@string/revanced_hide_action_button_index_3_summary_on" android:summaryOff="@string/revanced_hide_action_button_index_3_summary_off" />
<SwitchPreference android:title="@string/revanced_hide_action_button_index_4_title" android:key="revanced_hide_action_button_index_4" android:summaryOn="@string/revanced_hide_action_button_index_4_summary_on" android:summaryOff="@string/revanced_hide_action_button_index_4_summary_off" />
<SwitchPreference android:title="@string/revanced_hide_action_button_index_5_title" android:key="revanced_hide_action_button_index_5" android:summaryOn="@string/revanced_hide_action_button_index_5_summary_on" android:summaryOff="@string/revanced_hide_action_button_index_5_summary_off" />
<SwitchPreference android:title="@string/revanced_hide_action_button_index_6_title" android:key="revanced_hide_action_button_index_6" android:summaryOn="@string/revanced_hide_action_button_index_6_summary_on" android:summaryOff="@string/revanced_hide_action_button_index_6_summary_off" />
<SwitchPreference android:title="@string/revanced_hide_action_button_index_7_title" android:key="revanced_hide_action_button_index_7" android:summaryOn="@string/revanced_hide_action_button_index_7_summary_on" android:summaryOff="@string/revanced_hide_action_button_index_7_summary_off" />
<PreferenceCategory android:title="@string/revanced_preference_category_hide_by_index_live" android:layout="@layout/revanced_settings_preferences_category"/>
<SwitchPreference android:title="@string/revanced_hide_action_button_index_0_title" android:key="revanced_hide_action_button_index_live_0" android:summaryOn="@string/revanced_hide_action_button_index_0_summary_on" android:summaryOff="@string/revanced_hide_action_button_index_0_summary_off" />
<SwitchPreference android:title="@string/revanced_hide_action_button_index_1_title" android:key="revanced_hide_action_button_index_live_1" android:summaryOn="@string/revanced_hide_action_button_index_1_summary_on" android:summaryOff="@string/revanced_hide_action_button_index_1_summary_off" />
<SwitchPreference android:title="@string/revanced_hide_action_button_index_2_title" android:key="revanced_hide_action_button_index_live_2" android:summaryOn="@string/revanced_hide_action_button_index_2_summary_on" android:summaryOff="@string/revanced_hide_action_button_index_2_summary_off" />
<SwitchPreference android:title="@string/revanced_hide_action_button_index_3_title" android:key="revanced_hide_action_button_index_live_3" android:summaryOn="@string/revanced_hide_action_button_index_3_summary_on" android:summaryOff="@string/revanced_hide_action_button_index_3_summary_off" />
<SwitchPreference android:title="@string/revanced_hide_action_button_index_4_title" android:key="revanced_hide_action_button_index_live_4" android:summaryOn="@string/revanced_hide_action_button_index_4_summary_on" android:summaryOff="@string/revanced_hide_action_button_index_4_summary_off" />
<SwitchPreference android:title="@string/revanced_hide_action_button_index_5_title" android:key="revanced_hide_action_button_index_live_5" android:summaryOn="@string/revanced_hide_action_button_index_5_summary_on" android:summaryOff="@string/revanced_hide_action_button_index_5_summary_off" />
<Preference android:title="@string/revanced_hide_action_button_index_about_title" android:selectable="false" android:summary="@string/revanced_hide_action_button_index_about_summary" />SETTINGS: HIDE_BUTTONS_BY_INDEX -->
<!-- SETTINGS: HIDE_ACTION_BUTTONS
<SwitchPreference android:title="@string/revanced_hide_thanks_button_title" android:key="revanced_hide_thanks_button" android:summaryOn="@string/revanced_hide_thanks_button_summary_on" android:summaryOff="@string/revanced_hide_thanks_button_summary_off" />
<PreferenceCategory android:title="@string/revanced_preference_category_experimental_flag" android:layout="@layout/revanced_settings_preferences_category"/>
<SwitchPreference android:title="@string/revanced_hide_action_button_index_title" android:key="revanced_hide_action_button_index" android:summaryOn="@string/revanced_hide_action_button_index_summary_on" android:summaryOff="@string/revanced_hide_action_button_index_summary_off" />
<ListPreference android:entries="@array/revanced_remix_button_index_entries" android:title="@string/revanced_remix_button_index_title" android:key="revanced_remix_button_index" android:entryValues="@array/revanced_remix_button_index_entry_values" />
</PreferenceScreen>SETTINGS: HIDE_ACTION_BUTTONS -->
<!-- SETTINGS: AMBIENT_MODE_CONTROLS