mirror of
https://github.com/inotia00/revanced-patches.git
synced 2025-04-30 14:44:30 +02:00
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:
parent
4358a4739b
commit
89920480c7
@ -2,6 +2,7 @@ package app.revanced.extension.shared.patches.client
|
|||||||
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import app.revanced.extension.shared.settings.BaseSettings
|
import app.revanced.extension.shared.settings.BaseSettings
|
||||||
|
import app.revanced.extension.shared.utils.PackageUtils
|
||||||
import org.apache.commons.lang3.ArrayUtils
|
import org.apache.commons.lang3.ArrayUtils
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
@ -73,6 +74,15 @@ object YouTubeAppClient {
|
|||||||
iOSUserAgent(PACKAGE_NAME_IOS_UNPLUGGED, CLIENT_VERSION_IOS_UNPLUGGED)
|
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
|
// ANDROID VR
|
||||||
/**
|
/**
|
||||||
* Video not playable: Kids
|
* 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/),
|
* [the App Store page of the YouTube app](https://www.meta.com/en-us/experiences/2002317119880945/),
|
||||||
* in the `Additional details` section.
|
* 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.
|
* 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
|
val friendlyName: String
|
||||||
) {
|
) {
|
||||||
|
ANDROID(
|
||||||
|
id = 3,
|
||||||
|
userAgent = USER_AGENT_ANDROID,
|
||||||
|
androidSdkVersion = Build.VERSION.SDK,
|
||||||
|
clientVersion = CLIENT_VERSION_ANDROID,
|
||||||
|
clientName = "ANDROID",
|
||||||
|
friendlyName = "Android"
|
||||||
|
),
|
||||||
ANDROID_VR(
|
ANDROID_VR(
|
||||||
id = 28,
|
id = 28,
|
||||||
deviceMake = DEVICE_MAKE_ANDROID_VR,
|
deviceMake = DEVICE_MAKE_ANDROID_VR,
|
||||||
|
@ -62,43 +62,20 @@ public class SpoofStreamingDataPatch extends BlockRequestPatch {
|
|||||||
return false;
|
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.
|
* Injection point.
|
||||||
*/
|
*/
|
||||||
public static void fetchStreams(String url, Map<String, String> requestHeaders) {
|
public static void fetchStreams(String url, Map<String, String> requestHeaders) {
|
||||||
if (SPOOF_STREAMING_DATA) {
|
if (SPOOF_STREAMING_DATA) {
|
||||||
try {
|
String id = Utils.getVideoIdFromRequest(url);
|
||||||
Uri uri = Uri.parse(url);
|
if (id == null) {
|
||||||
String path = uri.getPath();
|
Logger.printException(() -> "Ignoring request with no id: " + url);
|
||||||
if (path == null || !path.contains("player")) {
|
return;
|
||||||
return;
|
} else if (id.isEmpty()) {
|
||||||
}
|
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
StreamingDataRequest.fetchRequest(id, requestHeaders, VISITOR_DATA, PO_TOKEN);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,6 +44,17 @@ object PlayerRoutes {
|
|||||||
"&alt=proto"
|
"&alt=proto"
|
||||||
).compile()
|
).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
|
@JvmField
|
||||||
val GET_VIDEO_DETAILS: CompiledRoute = Route(
|
val GET_VIDEO_DETAILS: CompiledRoute = Route(
|
||||||
Route.Method.POST,
|
Route.Method.POST,
|
||||||
|
@ -110,20 +110,13 @@ class StreamingDataRequest private constructor(
|
|||||||
private const val HTTP_TIMEOUT_MILLISECONDS = 10 * 1000
|
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
|
private const val MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000
|
||||||
|
|
||||||
@GuardedBy("itself")
|
@GuardedBy("itself")
|
||||||
val cache: MutableMap<String, StreamingDataRequest> = Collections.synchronizedMap(
|
val cache: MutableMap<String, StreamingDataRequest> = Collections.synchronizedMap(
|
||||||
object : LinkedHashMap<String, StreamingDataRequest>(100) {
|
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
|
private val CACHE_LIMIT = 50
|
||||||
|
|
||||||
override fun removeEldestEntry(eldest: Map.Entry<String, StreamingDataRequest>): Boolean {
|
override fun removeEldestEntry(eldest: Map.Entry<String, StreamingDataRequest>): Boolean {
|
||||||
|
@ -11,6 +11,7 @@ import android.content.res.Configuration;
|
|||||||
import android.content.res.Resources;
|
import android.content.res.Resources;
|
||||||
import android.net.ConnectivityManager;
|
import android.net.ConnectivityManager;
|
||||||
import android.net.NetworkInfo;
|
import android.net.NetworkInfo;
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
@ -432,6 +433,34 @@ public class Utils {
|
|||||||
setEditTextDialogTheme(builder, false);
|
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],
|
* If {@link Fragment} uses [Android library] rather than [AndroidX library],
|
||||||
* the Dialog theme corresponding to [Android library] should be used.
|
* the Dialog theme corresponding to [Android library] should be used.
|
||||||
|
@ -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.ByteArrayFilterGroupList;
|
||||||
import app.revanced.extension.shared.patches.components.Filter;
|
import app.revanced.extension.shared.patches.components.Filter;
|
||||||
import app.revanced.extension.shared.patches.components.StringFilterGroup;
|
import app.revanced.extension.shared.patches.components.StringFilterGroup;
|
||||||
|
import app.revanced.extension.shared.settings.BooleanSetting;
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
@ -18,6 +19,8 @@ public final class ActionButtonsFilter extends Filter {
|
|||||||
private final StringFilterGroup likeSubscribeGlow;
|
private final StringFilterGroup likeSubscribeGlow;
|
||||||
private final ByteArrayFilterGroupList bufferButtonsGroupList = new ByteArrayFilterGroupList();
|
private final ByteArrayFilterGroupList bufferButtonsGroupList = new ByteArrayFilterGroupList();
|
||||||
|
|
||||||
|
private static final boolean HIDE_ACTION_BUTTON_INDEX = Settings.HIDE_ACTION_BUTTON_INDEX.get();
|
||||||
|
|
||||||
public ActionButtonsFilter() {
|
public ActionButtonsFilter() {
|
||||||
actionBarRule = new StringFilterGroup(
|
actionBarRule = new StringFilterGroup(
|
||||||
null,
|
null,
|
||||||
@ -95,6 +98,9 @@ public final class ActionButtonsFilter extends Filter {
|
|||||||
@Override
|
@Override
|
||||||
public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray,
|
public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray,
|
||||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||||
|
if (HIDE_ACTION_BUTTON_INDEX) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (!path.startsWith(VIDEO_ACTION_BAR_PATH_PREFIX)) {
|
if (!path.startsWith(VIDEO_ACTION_BAR_PATH_PREFIX)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -2,53 +2,144 @@ package app.revanced.extension.youtube.patches.player;
|
|||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.ArrayUtils;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
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.settings.BooleanSetting;
|
||||||
import app.revanced.extension.shared.utils.Logger;
|
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.settings.Settings;
|
||||||
import app.revanced.extension.youtube.shared.VideoInformation;
|
import app.revanced.extension.youtube.shared.VideoInformation;
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings({"unused", "deprecation"})
|
||||||
public class ActionButtonsPatch {
|
public class ActionButtonsPatch {
|
||||||
|
|
||||||
public enum ActionButton {
|
public enum ActionButton {
|
||||||
INDEX_7(Settings.HIDE_ACTION_BUTTON_INDEX_7, Settings.HIDE_ACTION_BUTTON_INDEX_LIVE_7, 7),
|
UNKNOWN(
|
||||||
INDEX_6(Settings.HIDE_ACTION_BUTTON_INDEX_6, Settings.HIDE_ACTION_BUTTON_INDEX_LIVE_6, 6),
|
null,
|
||||||
INDEX_5(Settings.HIDE_ACTION_BUTTON_INDEX_5, Settings.HIDE_ACTION_BUTTON_INDEX_LIVE_5, 5),
|
null
|
||||||
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),
|
CLIP(
|
||||||
INDEX_2(Settings.HIDE_ACTION_BUTTON_INDEX_2, Settings.HIDE_ACTION_BUTTON_INDEX_LIVE_2, 2),
|
"clipButtonViewModel",
|
||||||
INDEX_1(Settings.HIDE_ACTION_BUTTON_INDEX_1, Settings.HIDE_ACTION_BUTTON_INDEX_LIVE_1, 1),
|
Settings.HIDE_CLIP_BUTTON
|
||||||
INDEX_0(Settings.HIDE_ACTION_BUTTON_INDEX_0, Settings.HIDE_ACTION_BUTTON_INDEX_LIVE_0, 0);
|
),
|
||||||
|
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;
|
@Nullable
|
||||||
private final BooleanSetting liveSetting;
|
public final String identifier;
|
||||||
private final int index;
|
@Nullable
|
||||||
|
public final BooleanSetting setting;
|
||||||
|
|
||||||
ActionButton(final BooleanSetting generalSetting, final BooleanSetting liveSetting, final int index) {
|
ActionButton(@Nullable String identifier, @Nullable BooleanSetting setting) {
|
||||||
this.generalSetting = generalSetting;
|
this.identifier = identifier;
|
||||||
this.liveSetting = liveSetting;
|
this.setting = setting;
|
||||||
this.index = index;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final String TARGET_COMPONENT_TYPE = "LazilyConvertedElement";
|
private static final String TARGET_COMPONENT_TYPE = "LazilyConvertedElement";
|
||||||
private static final String VIDEO_ACTION_BAR_PATH_PREFIX = "video_action_bar.eml";
|
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) {
|
public static List<Object> hideActionButtonByIndex(@Nullable List<Object> list, @Nullable String identifier) {
|
||||||
try {
|
try {
|
||||||
if (identifier != null &&
|
if (HIDE_ACTION_BUTTON_INDEX &&
|
||||||
|
identifier != null &&
|
||||||
identifier.startsWith(VIDEO_ACTION_BAR_PATH_PREFIX) &&
|
identifier.startsWith(VIDEO_ACTION_BAR_PATH_PREFIX) &&
|
||||||
list != null &&
|
list != null &&
|
||||||
!list.isEmpty() &&
|
!list.isEmpty() &&
|
||||||
list.get(0).toString().equals(TARGET_COMPONENT_TYPE)
|
list.get(0).toString().equals(TARGET_COMPONENT_TYPE)
|
||||||
) {
|
) {
|
||||||
final int size = list.size();
|
final int listSize = list.size();
|
||||||
final boolean isLive = VideoInformation.getLiveStreamState();
|
final String videoId = VideoInformation.getVideoId();
|
||||||
for (ActionButton button : ActionButton.values()) {
|
ActionButtonRequest request = ActionButtonRequest.getRequestForVideoId(videoId);
|
||||||
if (size > button.index && (isLive ? button.liveSetting.get() : button.generalSetting.get())) {
|
if (request != null) {
|
||||||
list.remove(button.index);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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_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_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 = new BooleanSetting("revanced_hide_action_button_index", FALSE, true);
|
||||||
public static final BooleanSetting HIDE_ACTION_BUTTON_INDEX_1 = new BooleanSetting("revanced_hide_action_button_index_1", FALSE);
|
public static final IntegerSetting REMIX_BUTTON_INDEX = new IntegerSetting("revanced_remix_button_index", 3, true, parent(HIDE_ACTION_BUTTON_INDEX));
|
||||||
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);
|
|
||||||
|
|
||||||
// PreferenceScreen: Player - Ambient mode
|
// PreferenceScreen: Player - Ambient mode
|
||||||
public static final BooleanSetting BYPASS_AMBIENT_MODE_RESTRICTIONS = new BooleanSetting("revanced_bypass_ambient_mode_restrictions", FALSE);
|
public static final BooleanSetting BYPASS_AMBIENT_MODE_RESTRICTIONS = new BooleanSetting("revanced_bypass_ambient_mode_restrictions", FALSE);
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package app.revanced.patches.youtube.player.action
|
package app.revanced.patches.youtube.player.action
|
||||||
|
|
||||||
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
|
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
|
||||||
import app.revanced.patcher.patch.booleanOption
|
|
||||||
import app.revanced.patcher.patch.bytecodePatch
|
import app.revanced.patcher.patch.bytecodePatch
|
||||||
import app.revanced.patches.shared.litho.addLithoFilter
|
import app.revanced.patches.shared.litho.addLithoFilter
|
||||||
import app.revanced.patches.shared.litho.emptyComponentsFingerprint
|
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.COMPONENTS_PATH
|
||||||
import app.revanced.patches.youtube.utils.extension.Constants.PLAYER_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.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.ResourceUtils.addPreference
|
||||||
import app.revanced.patches.youtube.utils.settings.settingsPatch
|
import app.revanced.patches.youtube.utils.settings.settingsPatch
|
||||||
import app.revanced.patches.youtube.video.information.videoInformationPatch
|
import app.revanced.patches.youtube.video.information.videoInformationPatch
|
||||||
import app.revanced.util.Utils.trimIndentMultiline
|
|
||||||
import app.revanced.util.addInstructionsAtControlFlowLabel
|
import app.revanced.util.addInstructionsAtControlFlowLabel
|
||||||
import app.revanced.util.findMethodOrThrow
|
import app.revanced.util.findMethodOrThrow
|
||||||
import app.revanced.util.fingerprint.methodOrThrow
|
import app.revanced.util.fingerprint.methodOrThrow
|
||||||
@ -44,72 +44,61 @@ val actionButtonsPatch = bytecodePatch(
|
|||||||
settingsPatch,
|
settingsPatch,
|
||||||
lithoFilterPatch,
|
lithoFilterPatch,
|
||||||
videoInformationPatch,
|
videoInformationPatch,
|
||||||
)
|
buildRequestPatch,
|
||||||
|
|
||||||
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
|
|
||||||
)
|
)
|
||||||
|
|
||||||
execute {
|
execute {
|
||||||
addLithoFilter(FILTER_CLASS_DESCRIPTOR)
|
addLithoFilter(FILTER_CLASS_DESCRIPTOR)
|
||||||
|
|
||||||
var settingArray = arrayOf(
|
// region patch for hide action buttons by index
|
||||||
"PREFERENCE_SCREEN: PLAYER",
|
|
||||||
"SETTINGS: HIDE_ACTION_BUTTONS"
|
|
||||||
)
|
|
||||||
|
|
||||||
if (hideActionButtonByIndex == true) {
|
componentListFingerprint.methodOrThrow(emptyComponentsFingerprint).apply {
|
||||||
componentListFingerprint.methodOrThrow(emptyComponentsFingerprint).apply {
|
val conversionContextToStringMethod =
|
||||||
val conversionContextToStringMethod =
|
findMethodOrThrow(parameters[1].type) {
|
||||||
findMethodOrThrow(parameters[1].type) {
|
name == "toString"
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
val identifierReference = with (conversionContextToStringMethod) {
|
||||||
val listIndex = implementation!!.instructions.lastIndex
|
val identifierStringIndex =
|
||||||
val listRegister = getInstruction<OneRegisterInstruction>(listIndex).registerA
|
indexOfFirstStringInstructionOrThrow(", identifierProperty=")
|
||||||
val identifierRegister = listRegister + 1
|
val identifierStringAppendIndex =
|
||||||
|
indexOfFirstInstructionOrThrow(identifierStringIndex, Opcode.INVOKE_VIRTUAL)
|
||||||
addInstructionsAtControlFlowLabel(
|
val identifierStringAppendIndexRegister = getInstruction<FiveRegisterInstruction>(identifierStringAppendIndex).registerD
|
||||||
listIndex, """
|
val identifierAppendIndex =
|
||||||
move-object/from16 v$identifierRegister, p2
|
indexOfFirstInstructionOrThrow(identifierStringAppendIndex + 1, Opcode.INVOKE_VIRTUAL)
|
||||||
iget-object v$identifierRegister, v$identifierRegister, $identifierReference
|
val identifierRegister = getInstruction<FiveRegisterInstruction>(identifierAppendIndex).registerD
|
||||||
invoke-static {v$listRegister, v$identifierRegister}, $ACTION_BUTTONS_CLASS_DESCRIPTOR->hideActionButtonByIndex(Ljava/util/List;Ljava/lang/String;)Ljava/util/List;
|
val identifierIndex = indexOfFirstInstructionReversedOrThrow(identifierAppendIndex) {
|
||||||
move-result-object v$listRegister
|
opcode == Opcode.IGET_OBJECT &&
|
||||||
"""
|
getReference<FieldReference>()?.type == "Ljava/lang/String;" &&
|
||||||
)
|
(this as? TwoRegisterInstruction)?.registerA == identifierRegister
|
||||||
|
}
|
||||||
settingArray += "SETTINGS: HIDE_BUTTONS_BY_INDEX"
|
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
|
// region add settings
|
||||||
|
|
||||||
addPreference(
|
addPreference(
|
||||||
settingArray,
|
arrayOf(
|
||||||
|
"PREFERENCE_SCREEN: PLAYER",
|
||||||
|
"SETTINGS: HIDE_ACTION_BUTTONS"
|
||||||
|
),
|
||||||
HIDE_ACTION_BUTTONS
|
HIDE_ACTION_BUTTONS
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -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(
|
internal val createStreamingDataFingerprint = legacyFingerprint(
|
||||||
name = "createStreamingDataFingerprint",
|
name = "createStreamingDataFingerprint",
|
||||||
accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR,
|
accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR,
|
||||||
|
@ -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.COMPATIBLE_PACKAGE
|
||||||
import app.revanced.patches.youtube.utils.compatibility.Constants.YOUTUBE_PACKAGE_NAME
|
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.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.ResourceUtils.addPreference
|
||||||
import app.revanced.patches.youtube.utils.settings.settingsPatch
|
import app.revanced.patches.youtube.utils.settings.settingsPatch
|
||||||
import app.revanced.util.findInstructionIndicesReversedOrThrow
|
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.AccessFlags
|
||||||
import com.android.tools.smali.dexlib2.Opcode
|
import com.android.tools.smali.dexlib2.Opcode
|
||||||
import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation
|
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.OneRegisterInstruction
|
||||||
import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
|
import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
|
||||||
import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction
|
import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction
|
||||||
@ -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.ImmutableMethod
|
||||||
import com.android.tools.smali.dexlib2.immutable.ImmutableMethodParameter
|
import com.android.tools.smali.dexlib2.immutable.ImmutableMethodParameter
|
||||||
|
|
||||||
const val EXTENSION_CLASS_DESCRIPTOR =
|
private const val EXTENSION_CLASS_DESCRIPTOR =
|
||||||
"$SPOOF_PATH/SpoofStreamingDataPatch;"
|
"$SPOOF_PATH/SpoofStreamingDataPatch;"
|
||||||
|
|
||||||
val spoofStreamingDataPatch = bytecodePatch(
|
val spoofStreamingDataPatch = bytecodePatch(
|
||||||
@ -52,36 +53,14 @@ val spoofStreamingDataPatch = bytecodePatch(
|
|||||||
settingsPatch,
|
settingsPatch,
|
||||||
baseSpoofUserAgentPatch(YOUTUBE_PACKAGE_NAME),
|
baseSpoofUserAgentPatch(YOUTUBE_PACKAGE_NAME),
|
||||||
blockRequestPatch,
|
blockRequestPatch,
|
||||||
|
buildRequestPatch,
|
||||||
)
|
)
|
||||||
|
|
||||||
execute {
|
execute {
|
||||||
|
|
||||||
// region Get replacement streams at player requests.
|
// region Get replacement streams at player requests.
|
||||||
|
|
||||||
buildRequestFingerprint.methodOrThrow().apply {
|
hookBuildRequest("$EXTENSION_CLASS_DESCRIPTOR->fetchStreams(Ljava/lang/String;Ljava/util/Map;)V")
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
|
@ -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"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -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;"
|
||||||
|
}
|
@ -435,6 +435,22 @@
|
|||||||
<item>4</item>
|
<item>4</item>
|
||||||
<item>5</item>
|
<item>5</item>
|
||||||
</string-array>
|
</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">
|
<string-array name="revanced_watch_history_type_entries">
|
||||||
<item>@string/revanced_watch_history_type_entry_1</item>
|
<item>@string/revanced_watch_history_type_entry_1</item>
|
||||||
<item>@string/revanced_watch_history_type_entry_2</item>
|
<item>@string/revanced_watch_history_type_entry_2</item>
|
||||||
|
@ -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_on">Thanks button is hidden.</string>
|
||||||
<string name="revanced_hide_thanks_button_summary_off">Thanks button is shown.</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 -->
|
<!-- PreferenceScreen: Player, PreferenceCategory: Player, PreferenceScreen: Action buttons, PreferenceCategory: Experimental flags -->
|
||||||
<string name="revanced_preference_category_hide_by_index">Hide by index</string>
|
<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>
|
Info:
|
||||||
<string name="revanced_hide_action_button_index_0_summary_on">First button is hidden.</string>
|
• Wrong action buttons may be hidden, or action buttons may not be hidden.
|
||||||
<string name="revanced_hide_action_button_index_0_summary_off">First button is shown.</string>
|
• Hiding action buttons leaves no empty space."</string>
|
||||||
<string name="revanced_hide_action_button_index_1_title">Hide second button</string>
|
<string name="revanced_hide_action_button_index_summary_off">"Action buttons are hidden by identifier filter.
|
||||||
<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>
|
|
||||||
|
|
||||||
<!-- PreferenceScreen: Player, PreferenceCategory: Player, PreferenceScreen: Action buttons, PreferenceCategory: Hide by index in live stream -->
|
Info:
|
||||||
<string name="revanced_preference_category_hide_by_index_live">Hide by index in live stream</string>
|
• Right action buttons are hidden.
|
||||||
|
• Hiding action buttons leaves empty space."</string>
|
||||||
<string name="revanced_hide_action_button_index_about_title">About Hide action button by index</string>
|
<string name="revanced_remix_button_index_title">Remix button 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>
|
|
||||||
|
|
||||||
<!-- PreferenceScreen: Player, PreferenceCategory: Player, PreferenceScreen: Ambient mode -->
|
<!-- PreferenceScreen: Player, PreferenceCategory: Player, PreferenceScreen: Ambient mode -->
|
||||||
<string name="revanced_preference_screen_ambient_mode_title">Ambient mode</string>
|
<string name="revanced_preference_screen_ambient_mode_title">Ambient mode</string>
|
||||||
|
@ -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_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_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_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 -->
|
<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"/>
|
||||||
<!-- SETTINGS: HIDE_BUTTONS_BY_INDEX
|
<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" />
|
||||||
<PreferenceCategory android:title="@string/revanced_preference_category_hide_by_index" android:layout="@layout/revanced_settings_preferences_category"/>
|
<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" />
|
||||||
<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
|
|
||||||
</PreferenceScreen>SETTINGS: HIDE_ACTION_BUTTONS -->
|
</PreferenceScreen>SETTINGS: HIDE_ACTION_BUTTONS -->
|
||||||
|
|
||||||
<!-- SETTINGS: AMBIENT_MODE_CONTROLS
|
<!-- SETTINGS: AMBIENT_MODE_CONTROLS
|
||||||
|
Loading…
x
Reference in New Issue
Block a user