requestHeader;
+
+ private static final String AUTHORIZATION_HEADER = "Authorization";
+
+ private static final String[] REQUEST_HEADER_KEYS = {
+ AUTHORIZATION_HEADER,
+ "X-GOOG-API-FORMAT-VERSION",
+ "X-Goog-Visitor-Id"
+ };
+
+ /**
+ * If the /get_watch request is not blocked,
+ * fetchRequest will not be invoked at the point where the video starts.
+ *
+ * An additional method is used to invoke fetchRequest in YouTube Music:
+ * 1. Save the requestHeader in a field.
+ * 2. Invoke fetchRequest with the videoId used in PlaybackStartDescriptor.
+ *
+ *
+ * @param requestHeaders Save the request Headers used for login to a field.
+ * Only used in YouTube Music where login is required.
+ */
+ private static void setRequestHeaders(Map requestHeaders) {
+ if (SPOOF_STREAMING_DATA_MUSIC) {
+ try {
+ // Save requestHeaders whenever an account is switched.
+ String authorization = requestHeaders.get(AUTHORIZATION_HEADER);
+ if (authorization == null || auth.equals(authorization)) {
+ return;
+ }
+ for (String key : REQUEST_HEADER_KEYS) {
+ if (requestHeaders.get(key) == null) {
+ return;
+ }
+ }
+ auth = authorization;
+ requestHeader = requestHeaders;
+ } catch (Exception ex) {
+ Logger.printException(() -> "setRequestHeaders failure", ex);
+ }
+ }
+ }
+
+ /**
+ * Injection point.
+ */
+ public static void fetchStreams(@NonNull String videoId) {
+ if (SPOOF_STREAMING_DATA_MUSIC) {
+ try {
+ if (requestHeader != null) {
+ StreamingDataRequest.fetchRequest(videoId, requestHeader, VISITOR_DATA, PO_TOKEN, droidGuardPoToken);
+ } else {
+ Logger.printDebug(() -> "Ignoring request with no header.");
+ }
+ } catch (Exception ex) {
+ Logger.printException(() -> "fetchStreams failure", ex);
+ }
+ }
+ }
+
/**
* Injection point.
*/
public static void fetchStreams(String url, Map requestHeaders) {
+ setRequestHeaders(requestHeaders);
+
if (SPOOF_STREAMING_DATA) {
try {
Uri uri = Uri.parse(url);
String path = uri.getPath();
+ if (path == null || !path.contains("player")) {
+ return;
+ }
+ // 'get_drm_license' has no video id and appears to happen when waiting for a paid video to start.
// 'heartbeat' has no video id and appears to be only after playback has started.
// 'refresh' has no video id and appears to happen when waiting for a livestream to start.
- if (path != null && path.contains("player") && !path.contains("heartbeat")
- && !path.contains("refresh")) {
- String id = uri.getQueryParameter("id");
- if (id == null) {
- Logger.printException(() -> "Ignoring request that has no video id." +
- " Url: " + url + " headers: " + requestHeaders);
- return;
- }
-
- StreamingDataRequest.fetchRequest(id, requestHeaders);
+ // 'ad_break' has no video id.
+ if (path.contains("get_drm_license") || path.contains("heartbeat") || path.contains("refresh") || path.contains("ad_break")) {
+ 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, droidGuardPoToken);
} catch (Exception ex) {
- Logger.printException(() -> "buildRequest failure", ex);
+ Logger.printException(() -> "fetchStreams failure", ex);
}
}
}
@@ -181,7 +264,7 @@ public class SpoofStreamingDataPatch {
* Called after {@link #getStreamingData(String)}.
*/
public static void setApproxDurationMs(String videoId, long approxDurationMs) {
- if (approxDurationMs != Long.MAX_VALUE) {
+ if (SPOOF_STREAMING_DATA_YOUTUBE && approxDurationMs != Long.MAX_VALUE) {
approxDurationMsMap.put(videoId, approxDurationMs);
Logger.printDebug(() -> "New approxDurationMs loaded, video id: " + videoId + ", video length: " + approxDurationMs);
}
@@ -203,11 +286,10 @@ public class SpoofStreamingDataPatch {
* Called after {@link #getStreamingData(String)}.
*/
public static long getApproxDurationMs(String videoId) {
- if (SPOOF_STREAMING_DATA && videoId != null) {
+ if (SPOOF_STREAMING_DATA_YOUTUBE && videoId != null) {
final Long approxDurationMs = approxDurationMsMap.get(videoId);
if (approxDurationMs != null) {
Logger.printDebug(() -> "Replacing video length: " + approxDurationMs + " for videoId: " + videoId);
- approxDurationMsMap.remove(videoId);
return approxDurationMs;
}
}
@@ -253,4 +335,17 @@ public class SpoofStreamingDataPatch {
return videoFormat;
}
+
+ /**
+ * Injection point.
+ */
+ public static void setDroidGuardPoToken(byte[] bytes) {
+ if (SPOOF_STREAMING_DATA && bytes.length > 20) {
+ final String poToken = Base64.encodeToString(bytes, Base64.URL_SAFE);
+ if (!droidGuardPoToken.equals(poToken)) {
+ Logger.printDebug(() -> "New droidGuardPoToken loaded:\n" + poToken);
+ droidGuardPoToken = poToken;
+ }
+ }
+ }
}
diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/requests/PlayerRoutes.java b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/requests/PlayerRoutes.java
deleted file mode 100644
index 4eb16d20c..000000000
--- a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/requests/PlayerRoutes.java
+++ /dev/null
@@ -1,95 +0,0 @@
-package app.revanced.extension.shared.patches.spoof.requests;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import java.io.IOException;
-import java.net.HttpURLConnection;
-
-import app.revanced.extension.shared.patches.client.AppClient.ClientType;
-import app.revanced.extension.shared.requests.Requester;
-import app.revanced.extension.shared.requests.Route;
-import app.revanced.extension.shared.utils.Logger;
-import app.revanced.extension.shared.utils.Utils;
-
-@SuppressWarnings({"ExtractMethodRecommender", "deprecation"})
-public final class PlayerRoutes {
- public static final Route.CompiledRoute GET_PLAYLIST_PAGE = new Route(
- Route.Method.POST,
- "next" +
- "?fields=contents.singleColumnWatchNextResults.playlist.playlist"
- ).compile();
- static final Route.CompiledRoute GET_STREAMING_DATA = new Route(
- Route.Method.POST,
- "player" +
- "?fields=streamingData" +
- "&alt=proto"
- ).compile();
- private static final String YT_API_URL = "https://youtubei.googleapis.com/youtubei/v1/";
- /**
- * TCP connection and HTTP read timeout
- */
- private static final int CONNECTION_TIMEOUT_MILLISECONDS = 10 * 1000; // 10 Seconds.
-
- private static final String LOCALE_LANGUAGE = Utils.getContext().getResources()
- .getConfiguration().locale.getLanguage();
-
- private PlayerRoutes() {
- }
-
- public static String createInnertubeBody(ClientType clientType) {
- return createInnertubeBody(clientType, false);
- }
-
- public static String createInnertubeBody(ClientType clientType, boolean playlistId) {
- JSONObject innerTubeBody = new JSONObject();
-
- try {
- JSONObject client = new JSONObject();
- client.put("clientName", clientType.clientName);
- client.put("clientVersion", clientType.clientVersion);
- client.put("deviceModel", clientType.deviceModel);
- client.put("osVersion", clientType.osVersion);
- if (clientType.androidSdkVersion != null) {
- client.put("androidSdkVersion", clientType.androidSdkVersion);
- client.put("osName", "Android");
- } else {
- client.put("deviceMake", "Apple");
- client.put("osName", "iOS");
- }
- client.put("hl", LOCALE_LANGUAGE);
-
- JSONObject context = new JSONObject();
- context.put("client", client);
-
- innerTubeBody.put("context", context);
- innerTubeBody.put("contentCheckOk", true);
- innerTubeBody.put("racyCheckOk", true);
- innerTubeBody.put("videoId", "%s");
- if (playlistId) {
- innerTubeBody.put("playlistId", "%s");
- }
- } catch (JSONException e) {
- Logger.printException(() -> "Failed to create innerTubeBody", e);
- }
-
- return innerTubeBody.toString();
- }
-
- /**
- * @noinspection SameParameterValue
- */
- public static HttpURLConnection getPlayerResponseConnectionFromRoute(Route.CompiledRoute route, ClientType clientType) throws IOException {
- var connection = Requester.getConnectionFromCompiledRoute(YT_API_URL, route);
-
- connection.setRequestProperty("Content-Type", "application/json");
- connection.setRequestProperty("User-Agent", clientType.userAgent);
-
- connection.setUseCaches(false);
- connection.setDoOutput(true);
-
- connection.setConnectTimeout(CONNECTION_TIMEOUT_MILLISECONDS);
- connection.setReadTimeout(CONNECTION_TIMEOUT_MILLISECONDS);
- return connection;
- }
-}
\ No newline at end of file
diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/requests/PlayerRoutes.kt b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/requests/PlayerRoutes.kt
new file mode 100644
index 000000000..fca54e4f2
--- /dev/null
+++ b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/requests/PlayerRoutes.kt
@@ -0,0 +1,176 @@
+package app.revanced.extension.shared.patches.spoof.requests
+
+import app.revanced.extension.shared.patches.client.AppClient
+import app.revanced.extension.shared.patches.client.WebClient
+import app.revanced.extension.shared.requests.Requester
+import app.revanced.extension.shared.requests.Route
+import app.revanced.extension.shared.requests.Route.CompiledRoute
+import app.revanced.extension.shared.utils.Logger
+import app.revanced.extension.shared.utils.Utils
+import org.apache.commons.lang3.StringUtils
+import org.json.JSONException
+import org.json.JSONObject
+import java.io.IOException
+import java.net.HttpURLConnection
+import java.nio.charset.StandardCharsets
+
+@Suppress("deprecation")
+object PlayerRoutes {
+ @JvmField
+ val GET_CATEGORY: CompiledRoute = Route(
+ Route.Method.POST,
+ "player" +
+ "?prettyPrint=false" +
+ "&fields=microformat.playerMicroformatRenderer.category"
+ ).compile()
+
+ @JvmField
+ val GET_PLAYLIST_PAGE: CompiledRoute = Route(
+ Route.Method.POST,
+ "next" +
+ "?prettyPrint=false" +
+ "&fields=contents.singleColumnWatchNextResults.playlist.playlist"
+ ).compile()
+
+ @JvmField
+ val GET_STREAMING_DATA: CompiledRoute = Route(
+ Route.Method.POST,
+ "player" +
+ "?fields=streamingData" +
+ "&alt=proto"
+ ).compile()
+
+ private const val YT_API_URL = "https://youtubei.googleapis.com/youtubei/v1/"
+
+ /**
+ * TCP connection and HTTP read timeout
+ */
+ private const val CONNECTION_TIMEOUT_MILLISECONDS = 10 * 1000 // 10 Seconds.
+
+ private val LOCALE_LANGUAGE: String = Utils.getContext().resources
+ .configuration.locale.language
+
+ @JvmStatic
+ fun createApplicationRequestBody(
+ clientType: AppClient.ClientType,
+ videoId: String,
+ playlistId: String? = null,
+ botGuardPoToken: String? = null,
+ visitorId: String? = null,
+ ): ByteArray {
+ val innerTubeBody = JSONObject()
+
+ try {
+ val client = JSONObject()
+ client.put("clientName", clientType.clientName)
+ client.put("clientVersion", clientType.clientVersion)
+ client.put("deviceMake", clientType.deviceMake)
+ client.put("deviceModel", clientType.deviceModel)
+ client.put("osName", clientType.osName)
+ client.put("osVersion", clientType.osVersion)
+ if (clientType.osName == "Android") {
+ client.put("androidSdkVersion", clientType.androidSdkVersion)
+ }
+ if (!clientType.supportsCookies) {
+ client.put("hl", LOCALE_LANGUAGE)
+ }
+
+ val context = JSONObject()
+ context.put("client", client)
+
+ innerTubeBody.put("context", context)
+ innerTubeBody.put("contentCheckOk", true)
+ innerTubeBody.put("racyCheckOk", true)
+ innerTubeBody.put("videoId", videoId)
+
+ if (playlistId != null) {
+ innerTubeBody.put("playlistId", playlistId)
+ }
+
+ if (!StringUtils.isAnyEmpty(botGuardPoToken, visitorId)) {
+ val serviceIntegrityDimensions = JSONObject()
+ serviceIntegrityDimensions.put("poToken", botGuardPoToken)
+ innerTubeBody.put("serviceIntegrityDimensions", serviceIntegrityDimensions)
+ }
+ } catch (e: JSONException) {
+ Logger.printException({ "Failed to create application innerTubeBody" }, e)
+ }
+
+ return innerTubeBody.toString().toByteArray(StandardCharsets.UTF_8)
+ }
+
+ @JvmStatic
+ fun createWebInnertubeBody(
+ clientType: WebClient.ClientType,
+ videoId: String
+ ): ByteArray {
+ val innerTubeBody = JSONObject()
+
+ try {
+ val client = JSONObject()
+ client.put("clientName", clientType.clientName)
+ client.put("clientVersion", clientType.clientVersion)
+ val context = JSONObject()
+ context.put("client", client)
+
+ val lockedSafetyMode = JSONObject()
+ lockedSafetyMode.put("lockedSafetyMode", false)
+ val user = JSONObject()
+ user.put("user", lockedSafetyMode)
+
+ innerTubeBody.put("context", context)
+ innerTubeBody.put("contentCheckOk", true)
+ innerTubeBody.put("racyCheckOk", true)
+ innerTubeBody.put("videoId", videoId)
+ } catch (e: JSONException) {
+ Logger.printException({ "Failed to create web innerTubeBody" }, e)
+ }
+
+ return innerTubeBody.toString().toByteArray(StandardCharsets.UTF_8)
+ }
+
+ @JvmStatic
+ fun getPlayerResponseConnectionFromRoute(
+ route: CompiledRoute,
+ clientType: AppClient.ClientType
+ ): HttpURLConnection {
+ return getPlayerResponseConnectionFromRoute(
+ route,
+ clientType.userAgent,
+ clientType.id.toString()
+ )
+ }
+
+ @JvmStatic
+ fun getPlayerResponseConnectionFromRoute(
+ route: CompiledRoute,
+ clientType: WebClient.ClientType
+ ): HttpURLConnection {
+ return getPlayerResponseConnectionFromRoute(
+ route,
+ clientType.userAgent,
+ clientType.id.toString()
+ )
+ }
+
+ @Throws(IOException::class)
+ fun getPlayerResponseConnectionFromRoute(
+ route: CompiledRoute,
+ userAgent: String,
+ clientVersion: String
+ ): HttpURLConnection {
+ val connection = Requester.getConnectionFromCompiledRoute(YT_API_URL, route)
+
+ connection.setRequestProperty("Content-Type", "application/json")
+ connection.setRequestProperty("User-Agent", userAgent)
+ connection.setRequestProperty("X-YouTube-Client-Version", clientVersion)
+
+ connection.useCaches = false
+ connection.doOutput = true
+
+ connection.connectTimeout = CONNECTION_TIMEOUT_MILLISECONDS
+ connection.readTimeout = CONNECTION_TIMEOUT_MILLISECONDS
+ return connection
+ }
+
+}
\ No newline at end of file
diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/requests/StreamingDataRequest.java b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/requests/StreamingDataRequest.java
deleted file mode 100644
index a76c8a2df..000000000
--- a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/requests/StreamingDataRequest.java
+++ /dev/null
@@ -1,241 +0,0 @@
-package app.revanced.extension.shared.patches.spoof.requests;
-
-import static app.revanced.extension.shared.patches.client.AppClient.getAvailableClientTypes;
-import static app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes.GET_STREAMING_DATA;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import java.io.BufferedInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.HttpURLConnection;
-import java.net.SocketTimeoutException;
-import java.nio.ByteBuffer;
-import java.nio.charset.StandardCharsets;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.LinkedHashMap;
-import java.util.Map;
-import java.util.Objects;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.Future;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-
-import app.revanced.extension.shared.patches.client.AppClient.ClientType;
-import app.revanced.extension.shared.settings.BaseSettings;
-import app.revanced.extension.shared.utils.Logger;
-import app.revanced.extension.shared.utils.Utils;
-
-/**
- * Video streaming data. Fetching is tied to the behavior YT uses,
- * where this class fetches the streams only when YT fetches.
- *
- * Effectively the cache expiration of these fetches is the same as the stock app,
- * since the stock app would not use expired streams and therefor
- * the extension replace stream hook is called only if YT
- * did use its own client streams.
- */
-public class StreamingDataRequest {
-
- private static final ClientType[] CLIENT_ORDER_TO_USE;
- private static final String AUTHORIZATION_HEADER = "Authorization";
- private static final String[] REQUEST_HEADER_KEYS = {
- AUTHORIZATION_HEADER, // Available only to logged-in users.
- "X-GOOG-API-FORMAT-VERSION",
- "X-Goog-Visitor-Id"
- };
- private static ClientType lastSpoofedClientType;
-
-
- /**
- * TCP connection and HTTP read timeout.
- */
- private static final int HTTP_TIMEOUT_MILLISECONDS = 10 * 1000;
- /**
- * Any arbitrarily large value, but must be at least twice {@link #HTTP_TIMEOUT_MILLISECONDS}
- */
- private static final int MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000;
- private static final Map cache = Collections.synchronizedMap(
- new LinkedHashMap<>(100) {
- /**
- * Cache limit must be greater than the maximum number of videos open at once,
- * which theoretically is more than 4 (3 Shorts + one regular minimized video).
- * But instead use a much larger value, to handle if a video viewed a while ago
- * is somehow still referenced. Each stream is a small array of Strings
- * so memory usage is not a concern.
- */
- private static final int CACHE_LIMIT = 50;
-
- @Override
- protected boolean removeEldestEntry(Entry eldest) {
- return size() > CACHE_LIMIT; // Evict the oldest entry if over the cache limit.
- }
- });
-
- public static String getLastSpoofedClientName() {
- return lastSpoofedClientType == null
- ? "Unknown"
- : lastSpoofedClientType.getFriendlyName();
- }
-
- static {
- ClientType[] allClientTypes = getAvailableClientTypes();
- ClientType preferredClient = BaseSettings.SPOOF_STREAMING_DATA_TYPE.get();
-
- if (Arrays.stream(allClientTypes).noneMatch(preferredClient::equals)) {
- CLIENT_ORDER_TO_USE = allClientTypes;
- } else {
- CLIENT_ORDER_TO_USE = new ClientType[allClientTypes.length];
- CLIENT_ORDER_TO_USE[0] = preferredClient;
-
- int i = 1;
- for (ClientType c : allClientTypes) {
- if (c != preferredClient) {
- CLIENT_ORDER_TO_USE[i++] = c;
- }
- }
- }
- }
-
- private final String videoId;
- private final Future future;
-
- private StreamingDataRequest(String videoId, Map playerHeaders) {
- Objects.requireNonNull(playerHeaders);
- this.videoId = videoId;
- this.future = Utils.submitOnBackgroundThread(() -> fetch(videoId, playerHeaders));
- }
-
- public static void fetchRequest(String videoId, Map fetchHeaders) {
- // Always fetch, even if there is an existing request for the same video.
- cache.put(videoId, new StreamingDataRequest(videoId, fetchHeaders));
- }
-
- @Nullable
- public static StreamingDataRequest getRequestForVideoId(String videoId) {
- return cache.get(videoId);
- }
-
- private static void handleConnectionError(String toastMessage, @Nullable Exception ex) {
- Logger.printInfo(() -> toastMessage, ex);
- }
-
- @Nullable
- private static HttpURLConnection send(ClientType clientType, String videoId,
- Map playerHeaders) {
- Objects.requireNonNull(clientType);
- Objects.requireNonNull(videoId);
- Objects.requireNonNull(playerHeaders);
-
- final long startTime = System.currentTimeMillis();
- Logger.printDebug(() -> "Fetching video streams for: " + videoId + " using client: " + clientType);
-
- try {
- HttpURLConnection connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(GET_STREAMING_DATA, clientType);
- connection.setConnectTimeout(HTTP_TIMEOUT_MILLISECONDS);
- connection.setReadTimeout(HTTP_TIMEOUT_MILLISECONDS);
-
- for (String key : REQUEST_HEADER_KEYS) {
- String value = playerHeaders.get(key);
- if (value != null) {
- if (key.equals(AUTHORIZATION_HEADER)) {
- if (!clientType.canLogin) {
- Logger.printDebug(() -> "Not including request header: " + key);
- continue;
- }
- }
-
- connection.setRequestProperty(key, value);
- }
- }
-
- String innerTubeBody = String.format(PlayerRoutes.createInnertubeBody(clientType), videoId);
- byte[] requestBody = innerTubeBody.getBytes(StandardCharsets.UTF_8);
- connection.setFixedLengthStreamingMode(requestBody.length);
- connection.getOutputStream().write(requestBody);
-
- final int responseCode = connection.getResponseCode();
- if (responseCode == 200) return connection;
-
- // This situation likely means the patches are outdated.
- // Use a toast message that suggests updating.
- handleConnectionError("Playback error (App is outdated?) " + clientType + ": "
- + responseCode + " response: " + connection.getResponseMessage(),
- null);
- } catch (SocketTimeoutException ex) {
- handleConnectionError("Connection timeout", ex);
- } catch (IOException ex) {
- handleConnectionError("Network error", ex);
- } catch (Exception ex) {
- Logger.printException(() -> "send failed", ex);
- } finally {
- Logger.printDebug(() -> "video: " + videoId + " took: " + (System.currentTimeMillis() - startTime) + "ms");
- }
-
- return null;
- }
-
- private static ByteBuffer fetch(String videoId, Map playerHeaders) {
- lastSpoofedClientType = null;
-
- // Retry with different client if empty response body is received.
- for (ClientType clientType : CLIENT_ORDER_TO_USE) {
- HttpURLConnection connection = send(clientType, videoId, playerHeaders);
- if (connection != null) {
- try {
- // gzip encoding doesn't response with content length (-1),
- // but empty response body does.
- if (connection.getContentLength() == 0) {
- Logger.printDebug(() -> "Received empty response" + "\nClient: " + clientType + "\nVideo: " + videoId);
- } else {
- try (InputStream inputStream = new BufferedInputStream(connection.getInputStream());
- ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
- byte[] buffer = new byte[2048];
- int bytesRead;
- while ((bytesRead = inputStream.read(buffer)) >= 0) {
- baos.write(buffer, 0, bytesRead);
- }
- lastSpoofedClientType = clientType;
-
- return ByteBuffer.wrap(baos.toByteArray());
- }
- }
- } catch (IOException ex) {
- Logger.printException(() -> "Fetch failed while processing response data", ex);
- }
- }
- }
-
- handleConnectionError("Could not fetch any client streams", null);
- return null;
- }
-
- public boolean fetchCompleted() {
- return future.isDone();
- }
-
- @Nullable
- public ByteBuffer getStream() {
- try {
- return future.get(MAX_MILLISECONDS_TO_WAIT_FOR_FETCH, TimeUnit.MILLISECONDS);
- } catch (TimeoutException ex) {
- Logger.printInfo(() -> "getStream timed out", ex);
- } catch (InterruptedException ex) {
- Logger.printException(() -> "getStream interrupted", ex);
- Thread.currentThread().interrupt(); // Restore interrupt status flag.
- } catch (ExecutionException ex) {
- Logger.printException(() -> "getStream failure", ex);
- }
-
- return null;
- }
-
- @NonNull
- @Override
- public String toString() {
- return "StreamingDataRequest{" + "videoId='" + videoId + '\'' + '}';
- }
-}
diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/requests/StreamingDataRequest.kt b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/requests/StreamingDataRequest.kt
new file mode 100644
index 000000000..6389ac1ea
--- /dev/null
+++ b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/requests/StreamingDataRequest.kt
@@ -0,0 +1,296 @@
+package app.revanced.extension.shared.patches.spoof.requests
+
+import androidx.annotation.GuardedBy
+import app.revanced.extension.shared.patches.client.AppClient
+import app.revanced.extension.shared.patches.client.AppClient.availableClientTypes
+import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes.GET_STREAMING_DATA
+import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes.createApplicationRequestBody
+import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes.getPlayerResponseConnectionFromRoute
+import app.revanced.extension.shared.settings.BaseSettings
+import app.revanced.extension.shared.utils.Logger
+import app.revanced.extension.shared.utils.Utils
+import org.apache.commons.lang3.StringUtils
+import java.io.BufferedInputStream
+import java.io.ByteArrayOutputStream
+import java.io.IOException
+import java.net.HttpURLConnection
+import java.net.SocketTimeoutException
+import java.nio.ByteBuffer
+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
+
+/**
+ * Video streaming data. Fetching is tied to the behavior YT uses,
+ * where this class fetches the streams only when YT fetches.
+ *
+ * Effectively the cache expiration of these fetches is the same as the stock app,
+ * since the stock app would not use expired streams and therefor
+ * the extension replace stream hook is called only if YT
+ * did use its own client streams.
+ */
+class StreamingDataRequest private constructor(
+ videoId: String, playerHeaders: Map, visitorId: String,
+ botGuardPoToken: String, droidGuardPoToken: String
+) {
+ private val videoId: String
+ private val future: Future
+
+ init {
+ Objects.requireNonNull(playerHeaders)
+ this.videoId = videoId
+ this.future = Utils.submitOnBackgroundThread {
+ fetch(
+ videoId,
+ playerHeaders,
+ visitorId,
+ botGuardPoToken,
+ droidGuardPoToken
+ )
+ }
+ }
+
+ fun fetchCompleted(): Boolean {
+ return future.isDone
+ }
+
+ val stream: ByteBuffer?
+ get() {
+ try {
+ return future[MAX_MILLISECONDS_TO_WAIT_FOR_FETCH.toLong(), TimeUnit.MILLISECONDS]
+ } catch (ex: TimeoutException) {
+ Logger.printInfo(
+ { "getStream timed out" },
+ ex
+ )
+ } catch (ex: InterruptedException) {
+ Logger.printException(
+ { "getStream interrupted" },
+ ex
+ )
+ Thread.currentThread().interrupt() // Restore interrupt status flag.
+ } catch (ex: ExecutionException) {
+ Logger.printException(
+ { "getStream failure" },
+ ex
+ )
+ }
+
+ return null
+ }
+
+ override fun toString(): String {
+ return "StreamingDataRequest{videoId='$videoId'}"
+ }
+
+ companion object {
+ private const val AUTHORIZATION_HEADER = "Authorization"
+ private const val VISITOR_ID_HEADER = "X-Goog-Visitor-Id"
+ private val REQUEST_HEADER_KEYS = arrayOf(
+ AUTHORIZATION_HEADER, // Available only to logged-in users.
+ "X-GOOG-API-FORMAT-VERSION",
+ VISITOR_ID_HEADER
+ )
+
+ private val CLIENT_ORDER_TO_USE: Array =
+ availableClientTypes(BaseSettings.SPOOF_STREAMING_DATA_TYPE.get())
+
+ private var lastSpoofedClientType: AppClient.ClientType? = null
+
+
+ /**
+ * TCP connection and HTTP read timeout.
+ */
+ private const val HTTP_TIMEOUT_MILLISECONDS = 10 * 1000
+
+ /**
+ * Any arbitrarily large value, but must be at least twice [.HTTP_TIMEOUT_MILLISECONDS]
+ */
+ private const val MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000
+
+ @GuardedBy("itself")
+ val cache: MutableMap = Collections.synchronizedMap(
+ object : LinkedHashMap(100) {
+ /**
+ * Cache limit must be greater than the maximum number of videos open at once,
+ * which theoretically is more than 4 (3 Shorts + one regular minimized video).
+ * But instead use a much larger value, to handle if a video viewed a while ago
+ * is somehow still referenced. Each stream is a small array of Strings
+ * so memory usage is not a concern.
+ */
+ private val CACHE_LIMIT = 50
+
+ override fun removeEldestEntry(eldest: Map.Entry): Boolean {
+ return size > CACHE_LIMIT // Evict the oldest entry if over the cache limit.
+ }
+ })
+
+ @JvmStatic
+ val lastSpoofedClientName: String
+ get() = lastSpoofedClientType
+ ?.friendlyName
+ ?: "Unknown"
+
+ @JvmStatic
+ fun fetchRequest(
+ videoId: String, fetchHeaders: Map, visitorId: String,
+ botGuardPoToken: String, droidGuardPoToken: String
+ ) {
+ // Always fetch, even if there is an existing request for the same video.
+ cache[videoId] =
+ StreamingDataRequest(
+ videoId,
+ fetchHeaders,
+ visitorId,
+ botGuardPoToken,
+ droidGuardPoToken
+ )
+ }
+
+ @JvmStatic
+ fun getRequestForVideoId(videoId: String): StreamingDataRequest? {
+ return cache[videoId]
+ }
+
+ private fun handleConnectionError(toastMessage: String, ex: Exception?) {
+ Logger.printInfo({ toastMessage }, ex)
+ }
+
+ private fun send(
+ clientType: AppClient.ClientType, videoId: String, playerHeaders: Map,
+ visitorId: String, botGuardPoToken: String, droidGuardPoToken: String
+ ): HttpURLConnection? {
+ Objects.requireNonNull(clientType)
+ Objects.requireNonNull(videoId)
+ Objects.requireNonNull(playerHeaders)
+
+ val startTime = System.currentTimeMillis()
+ Logger.printDebug { "Fetching video streams for: $videoId using client: $clientType" }
+
+ try {
+ val connection =
+ getPlayerResponseConnectionFromRoute(GET_STREAMING_DATA, clientType)
+ connection.connectTimeout = HTTP_TIMEOUT_MILLISECONDS
+ connection.readTimeout = HTTP_TIMEOUT_MILLISECONDS
+
+ val usePoToken =
+ clientType.requirePoToken && !StringUtils.isAnyEmpty(botGuardPoToken, visitorId)
+
+ for (key in REQUEST_HEADER_KEYS) {
+ var value = playerHeaders[key]
+ if (value != null) {
+ if (key == AUTHORIZATION_HEADER) {
+ if (!clientType.supportsCookies) {
+ Logger.printDebug { "Not including request header: $key" }
+ continue
+ }
+ }
+ if (key == VISITOR_ID_HEADER && usePoToken) {
+ val originalVisitorId: String = value
+ Logger.printDebug { "Original visitor id:\n$originalVisitorId" }
+ Logger.printDebug { "Replaced visitor id:\n$visitorId" }
+ value = visitorId
+ }
+
+ connection.setRequestProperty(key, value)
+ }
+ }
+
+ val requestBody: ByteArray
+ if (usePoToken) {
+ requestBody = createApplicationRequestBody(
+ clientType = clientType,
+ videoId = videoId,
+ botGuardPoToken = botGuardPoToken,
+ visitorId = visitorId
+ )
+ if (droidGuardPoToken.isNotEmpty()) {
+ Logger.printDebug { "Original poToken (droidGuardPoToken):\n$droidGuardPoToken" }
+ }
+ Logger.printDebug { "Replaced poToken (botGuardPoToken):\n$botGuardPoToken" }
+ } else {
+ requestBody =
+ createApplicationRequestBody(clientType = clientType, videoId = videoId)
+ }
+ connection.setFixedLengthStreamingMode(requestBody.size)
+ connection.outputStream.write(requestBody)
+
+ val responseCode = connection.responseCode
+ if (responseCode == 200) return connection
+
+ // This situation likely means the patches are outdated.
+ // Use a toast message that suggests updating.
+ handleConnectionError(
+ ("Playback error (App is outdated?) " + clientType + ": "
+ + responseCode + " response: " + connection.responseMessage),
+ null
+ )
+ } catch (ex: SocketTimeoutException) {
+ handleConnectionError("Connection timeout", ex)
+ } catch (ex: IOException) {
+ handleConnectionError("Network error", ex)
+ } catch (ex: Exception) {
+ Logger.printException({ "send failed" }, ex)
+ } finally {
+ Logger.printDebug { "video: " + videoId + " took: " + (System.currentTimeMillis() - startTime) + "ms" }
+ }
+
+ return null
+ }
+
+ private fun fetch(
+ videoId: String, playerHeaders: Map, visitorId: String,
+ botGuardPoToken: String, droidGuardPoToken: String
+ ): ByteBuffer? {
+ lastSpoofedClientType = null
+
+ // Retry with different client if empty response body is received.
+ for (clientType in CLIENT_ORDER_TO_USE) {
+ if (clientType.requireAuth &&
+ playerHeaders[AUTHORIZATION_HEADER] == null
+ ) {
+ Logger.printDebug { "Skipped login-required client (incognito mode or not logged in)\nClient: $clientType\nVideo: $videoId" }
+ continue
+ }
+ send(
+ clientType,
+ videoId,
+ playerHeaders,
+ visitorId,
+ botGuardPoToken,
+ droidGuardPoToken
+ )?.let { connection ->
+ try {
+ // gzip encoding doesn't response with content length (-1),
+ // but empty response body does.
+ if (connection.contentLength == 0) {
+ Logger.printDebug { "Received empty response\nClient: $clientType\nVideo: $videoId" }
+ } else {
+ BufferedInputStream(connection.inputStream).use { inputStream ->
+ ByteArrayOutputStream().use { stream ->
+ val buffer = ByteArray(2048)
+ var bytesRead: Int
+ while ((inputStream.read(buffer)
+ .also { bytesRead = it }) >= 0
+ ) {
+ stream.write(buffer, 0, bytesRead)
+ }
+ lastSpoofedClientType = clientType
+ return ByteBuffer.wrap(stream.toByteArray())
+ }
+ }
+ }
+ } catch (ex: IOException) {
+ Logger.printException({ "Fetch failed while processing response data" }, ex)
+ }
+ }
+ }
+
+ handleConnectionError("Could not fetch any client streams", null)
+ return null
+ }
+ }
+}
diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/returnyoutubedislike/requests/RYDVoteData.java b/extensions/shared/src/main/java/app/revanced/extension/shared/returnyoutubedislike/requests/RYDVoteData.java
index a4a56de04..984fa5042 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/shared/returnyoutubedislike/requests/RYDVoteData.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/shared/returnyoutubedislike/requests/RYDVoteData.java
@@ -111,22 +111,21 @@ public final class RYDVoteData {
public void updateUsingVote(Vote vote) {
final int likesToAdd, dislikesToAdd;
- switch (vote) {
- case LIKE:
+ dislikesToAdd = switch (vote) {
+ case LIKE -> {
likesToAdd = 1;
- dislikesToAdd = 0;
- break;
- case DISLIKE:
+ yield 0;
+ }
+ case DISLIKE -> {
likesToAdd = 0;
- dislikesToAdd = 1;
- break;
- case LIKE_REMOVE:
+ yield 1;
+ }
+ case LIKE_REMOVE -> {
likesToAdd = 0;
- dislikesToAdd = 0;
- break;
- default:
- throw new IllegalStateException();
- }
+ yield 0;
+ }
+ default -> throw new IllegalStateException();
+ };
// If a video has no public likes but RYD has raw like data,
// then use the raw data instead.
diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/returnyoutubedislike/requests/ReturnYouTubeDislikeApi.java b/extensions/shared/src/main/java/app/revanced/extension/shared/returnyoutubedislike/requests/ReturnYouTubeDislikeApi.java
index df1e503b5..9cbf96c86 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/shared/returnyoutubedislike/requests/ReturnYouTubeDislikeApi.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/shared/returnyoutubedislike/requests/ReturnYouTubeDislikeApi.java
@@ -27,6 +27,7 @@ import app.revanced.extension.shared.returnyoutubedislike.ReturnYouTubeDislike;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.Utils;
+@SuppressWarnings("All")
public class ReturnYouTubeDislikeApi {
/**
* {@link #fetchVotes(String)} TCP connection timeout
@@ -81,9 +82,87 @@ public class ReturnYouTubeDislikeApi {
public static boolean toastOnConnectionError = false;
+ /**
+ * Number of times {@link #HTTP_STATUS_CODE_RATE_LIMIT} was requested by RYD api.
+ * Does not include network calls attempted while rate limit is in effect,
+ * and does not include rate limit imposed if a fetch fails.
+ */
+ private static volatile int numberOfRateLimitRequestsEncountered;
+
+ /**
+ * Number of network calls made in {@link #fetchVotes(String)}
+ */
+ private static volatile int fetchCallCount;
+
+ /**
+ * Number of times {@link #fetchVotes(String)} failed due to timeout or any other error.
+ * This does not include when rate limit requests are encountered.
+ */
+ private static volatile int fetchCallNumberOfFailures;
+
+ /**
+ * Total time spent waiting for {@link #fetchVotes(String)} network call to complete.
+ * Value does does not persist on app shut down.
+ */
+ private static volatile long fetchCallResponseTimeTotal;
+
+ /**
+ * Round trip network time for the most recent call to {@link #fetchVotes(String)}
+ */
+ private static volatile long fetchCallResponseTimeLast;
+ private static volatile long fetchCallResponseTimeMin;
+ private static volatile long fetchCallResponseTimeMax;
+
+ public static final int FETCH_CALL_RESPONSE_TIME_VALUE_RATE_LIMIT = -1;
+
+ /**
+ * If rate limit was hit, this returns {@link #FETCH_CALL_RESPONSE_TIME_VALUE_RATE_LIMIT}
+ */
+ public static long getFetchCallResponseTimeLast() {
+ return fetchCallResponseTimeLast;
+ }
+
+ public static long getFetchCallResponseTimeMin() {
+ return fetchCallResponseTimeMin;
+ }
+
+ public static long getFetchCallResponseTimeMax() {
+ return fetchCallResponseTimeMax;
+ }
+
+ public static long getFetchCallResponseTimeAverage() {
+ return fetchCallCount == 0 ? 0 : (fetchCallResponseTimeTotal / fetchCallCount);
+ }
+
+ public static int getFetchCallCount() {
+ return fetchCallCount;
+ }
+
+ public static int getFetchCallNumberOfFailures() {
+ return fetchCallNumberOfFailures;
+ }
+
+ public static int getNumberOfRateLimitRequestsEncountered() {
+ return numberOfRateLimitRequestsEncountered;
+ }
+
private ReturnYouTubeDislikeApi() {
} // utility class
+ /**
+ * Simulates a slow response by doing meaningless calculations.
+ * Used to debug the app UI and verify UI timeout logic works
+ */
+ private static void randomlyWaitIfLocallyDebugging() {
+ final boolean DEBUG_RANDOMLY_DELAY_NETWORK_CALLS = false; // set true to debug UI
+
+ if (DEBUG_RANDOMLY_DELAY_NETWORK_CALLS) {
+ final long amountOfTimeToWaste = (long) (Math.random()
+ * (API_GET_VOTES_TCP_TIMEOUT_MILLISECONDS + API_GET_VOTES_HTTP_TIMEOUT_MILLISECONDS));
+ Utils.doNothingForDuration(amountOfTimeToWaste);
+ }
+ }
+
/**
* Clears any backoff rate limits in effect.
* Should be called if RYD is turned on/off.
@@ -116,41 +195,64 @@ public class ReturnYouTubeDislikeApi {
* @return True, if a client rate limit was requested
*/
private static boolean checkIfRateLimitWasHit(int httpResponseCode) {
+ final boolean DEBUG_RATE_LIMIT = false; // set to true, to verify rate limit works
+
+ if (DEBUG_RATE_LIMIT) {
+ final double RANDOM_RATE_LIMIT_PERCENTAGE = 0.2; // 20% chance of a triggering a rate limit
+ if (Math.random() < RANDOM_RATE_LIMIT_PERCENTAGE) {
+ Logger.printDebug(() -> "Artificially triggering rate limit for debug purposes");
+ httpResponseCode = HTTP_STATUS_CODE_RATE_LIMIT;
+ }
+ }
return httpResponseCode == HTTP_STATUS_CODE_RATE_LIMIT;
}
- private static void updateRateLimitAndStats(boolean connectionError, boolean rateLimitHit) {
+ @SuppressWarnings("NonAtomicOperationOnVolatileField") // Don't care, fields are only estimates.
+ private static void updateRateLimitAndStats(long timeNetworkCallStarted, boolean connectionError, boolean rateLimitHit) {
if (connectionError && rateLimitHit) {
throw new IllegalArgumentException();
}
+ final long responseTimeOfFetchCall = System.currentTimeMillis() - timeNetworkCallStarted;
+ fetchCallResponseTimeTotal += responseTimeOfFetchCall;
+ fetchCallResponseTimeMin = (fetchCallResponseTimeMin == 0) ? responseTimeOfFetchCall : Math.min(responseTimeOfFetchCall, fetchCallResponseTimeMin);
+ fetchCallResponseTimeMax = Math.max(responseTimeOfFetchCall, fetchCallResponseTimeMax);
+ fetchCallCount++;
if (connectionError) {
timeToResumeAPICalls = System.currentTimeMillis() + BACKOFF_CONNECTION_ERROR_MILLISECONDS;
+ fetchCallResponseTimeLast = responseTimeOfFetchCall;
+ fetchCallNumberOfFailures++;
lastApiCallFailed = true;
} else if (rateLimitHit) {
Logger.printDebug(() -> "API rate limit was hit. Stopping API calls for the next "
+ BACKOFF_RATE_LIMIT_MILLISECONDS + " seconds");
timeToResumeAPICalls = System.currentTimeMillis() + BACKOFF_RATE_LIMIT_MILLISECONDS;
+ numberOfRateLimitRequestsEncountered++;
+ fetchCallResponseTimeLast = FETCH_CALL_RESPONSE_TIME_VALUE_RATE_LIMIT;
if (!lastApiCallFailed && toastOnConnectionError) {
Utils.showToastLong(str("revanced_ryd_failure_client_rate_limit_requested"));
}
lastApiCallFailed = true;
} else {
+ fetchCallResponseTimeLast = responseTimeOfFetchCall;
lastApiCallFailed = false;
}
}
- private static void handleConnectionError(@NonNull String toastMessage, @Nullable Exception ex) {
+ private static void handleConnectionError(@NonNull String toastMessage,
+ @Nullable Exception ex,
+ boolean showLongToast) {
if (!lastApiCallFailed && toastOnConnectionError) {
- Utils.showToastShort(toastMessage);
- }
- if (ex != null) {
- Logger.printInfo(() -> toastMessage, ex);
+ if (showLongToast) {
+ Utils.showToastLong(toastMessage);
+ } else {
+ Utils.showToastShort(toastMessage);
+ }
}
+ lastApiCallFailed = true;
+
+ Logger.printInfo(() -> toastMessage, ex);
}
- /**
- * @return NULL if fetch failed, or if a rate limit is in effect.
- */
@Nullable
public static RYDVoteData fetchVotes(String videoId) {
Utils.verifyOffMainThread();
@@ -160,6 +262,7 @@ public class ReturnYouTubeDislikeApi {
return null;
}
Logger.printDebug(() -> "Fetching votes for: " + videoId);
+ final long timeNetworkCallStarted = System.currentTimeMillis();
try {
HttpURLConnection connection = getRYDConnectionFromRoute(ReturnYouTubeDislikeRoutes.GET_DISLIKES, videoId);
@@ -173,10 +276,12 @@ public class ReturnYouTubeDislikeApi {
connection.setConnectTimeout(API_GET_VOTES_TCP_TIMEOUT_MILLISECONDS); // timeout for TCP connection to server
connection.setReadTimeout(API_GET_VOTES_HTTP_TIMEOUT_MILLISECONDS); // timeout for server response
+ randomlyWaitIfLocallyDebugging();
+
final int responseCode = connection.getResponseCode();
if (checkIfRateLimitWasHit(responseCode)) {
connection.disconnect(); // rate limit hit, should disconnect
- updateRateLimitAndStats(false, true);
+ updateRateLimitAndStats(timeNetworkCallStarted, false, true);
return null;
}
@@ -185,7 +290,7 @@ public class ReturnYouTubeDislikeApi {
JSONObject json = Requester.parseJSONObject(connection);
try {
RYDVoteData votingData = new RYDVoteData(json);
- updateRateLimitAndStats(false, false);
+ updateRateLimitAndStats(timeNetworkCallStarted, false, false);
Logger.printDebug(() -> "Voting data fetched: " + votingData);
return votingData;
} catch (JSONException ex) {
@@ -193,20 +298,21 @@ public class ReturnYouTubeDislikeApi {
// fall thru to update statistics
}
} else {
- handleConnectionError(str("revanced_ryd_failure_connection_status_code", responseCode), null);
+ // Unexpected response code. Most likely RYD is temporarily broken.
+ handleConnectionError(str("revanced_ryd_failure_connection_status_code", responseCode),
+ null, true);
}
- connection.disconnect(); // something went wrong, might as well disconnect
- } catch (
- SocketTimeoutException ex) { // connection timed out, response timeout, or some other network error
- handleConnectionError((str("revanced_ryd_failure_connection_timeout")), ex);
+ connection.disconnect(); // Something went wrong, might as well disconnect.
+ } catch (SocketTimeoutException ex) {
+ handleConnectionError((str("revanced_ryd_failure_connection_timeout")), ex, false);
} catch (IOException ex) {
- handleConnectionError((str("revanced_ryd_failure_generic", ex.getMessage())), ex);
+ handleConnectionError((str("revanced_ryd_failure_generic", ex.getMessage())), ex, true);
} catch (Exception ex) {
// should never happen
Logger.printException(() -> "fetchVotes failure", ex);
}
- updateRateLimitAndStats(true, false);
+ updateRateLimitAndStats(timeNetworkCallStarted, true, false);
return null;
}
@@ -220,7 +326,7 @@ public class ReturnYouTubeDislikeApi {
if (checkIfRateLimitInEffect("registerAsNewUser")) {
return null;
}
- String userId = randomString();
+ String userId = randomString(36);
Logger.printDebug(() -> "Trying to register new user");
HttpURLConnection connection = getRYDConnectionFromRoute(ReturnYouTubeDislikeRoutes.GET_REGISTRATION, userId);
@@ -241,12 +347,13 @@ public class ReturnYouTubeDislikeApi {
String solution = solvePuzzle(challenge, difficulty);
return confirmRegistration(userId, solution);
}
- handleConnectionError(str("revanced_ryd_failure_connection_status_code", responseCode), null);
+ handleConnectionError(str("revanced_ryd_failure_connection_status_code", responseCode),
+ null, true);
connection.disconnect();
} catch (SocketTimeoutException ex) {
- handleConnectionError(str("revanced_ryd_failure_connection_timeout"), ex);
+ handleConnectionError(str("revanced_ryd_failure_connection_timeout"), ex, false);
} catch (IOException ex) {
- handleConnectionError(str("revanced_ryd_failure_generic", "registration failed"), ex);
+ handleConnectionError(str("revanced_ryd_failure_generic", "registration failed"), ex, true);
} catch (Exception ex) {
Logger.printException(() -> "Failed to register user", ex); // should never happen
}
@@ -283,15 +390,18 @@ public class ReturnYouTubeDislikeApi {
Logger.printDebug(() -> "Registration confirmation successful");
return userId;
}
+
// Something went wrong, might as well disconnect.
String response = Requester.parseStringAndDisconnect(connection);
Logger.printInfo(() -> "Failed to confirm registration for user: " + userId
+ " solution: " + solution + " responseCode: " + responseCode + " response: '" + response + "''");
- handleConnectionError(str("revanced_ryd_failure_connection_status_code", responseCode), null);
+ handleConnectionError(str("revanced_ryd_failure_connection_status_code", responseCode),
+ null, true);
} catch (SocketTimeoutException ex) {
- handleConnectionError(str("revanced_ryd_failure_connection_timeout"), ex);
+ handleConnectionError(str("revanced_ryd_failure_connection_timeout"), ex, false);
} catch (IOException ex) {
- handleConnectionError(str("revanced_ryd_failure_generic", "confirm registration failed"), ex);
+ handleConnectionError(str("revanced_ryd_failure_generic", "confirm registration failed"),
+ ex, true);
} catch (Exception ex) {
Logger.printException(() -> "Failed to confirm registration for user: " + userId
+ "solution: " + solution, ex);
@@ -299,16 +409,16 @@ public class ReturnYouTubeDislikeApi {
return null;
}
- public static void sendVote(String userId, String videoId, ReturnYouTubeDislike.Vote vote) {
+ public static boolean sendVote(String userId, String videoId, ReturnYouTubeDislike.Vote vote) {
Utils.verifyOffMainThread();
Objects.requireNonNull(videoId);
Objects.requireNonNull(vote);
try {
- if (userId == null) return;
+ if (userId == null) return false;
if (checkIfRateLimitInEffect("sendVote")) {
- return;
+ return false;
}
Logger.printDebug(() -> "Trying to vote for video: " + videoId + " with vote: " + vote);
@@ -325,7 +435,7 @@ public class ReturnYouTubeDislikeApi {
final int responseCode = connection.getResponseCode();
if (checkIfRateLimitWasHit(responseCode)) {
connection.disconnect(); // disconnect, as no more connections will be made for a little while
- return;
+ return false;
}
if (responseCode == HTTP_STATUS_CODE_SUCCESS) {
JSONObject json = Requester.parseJSONObject(connection);
@@ -333,25 +443,26 @@ public class ReturnYouTubeDislikeApi {
int difficulty = json.getInt("difficulty");
String solution = solvePuzzle(challenge, difficulty);
- confirmVote(videoId, userId, solution);
- return;
+ return confirmVote(videoId, userId, solution);
}
Logger.printInfo(() -> "Failed to send vote for video: " + videoId + " vote: " + vote
+ " response code was: " + responseCode);
- handleConnectionError(str("revanced_ryd_failure_connection_status_code", responseCode), null);
+ handleConnectionError(str("revanced_ryd_failure_connection_status_code", responseCode),
+ null, true);
connection.disconnect(); // something went wrong, might as well disconnect
} catch (SocketTimeoutException ex) {
- handleConnectionError(str("revanced_ryd_failure_connection_timeout"), ex);
+ handleConnectionError(str("revanced_ryd_failure_connection_timeout"), ex, false);
} catch (IOException ex) {
- handleConnectionError(str("revanced_ryd_failure_generic", "send vote failed"), ex);
+ handleConnectionError(str("revanced_ryd_failure_generic", "send vote failed"), ex, true);
} catch (Exception ex) {
// should never happen
Logger.printException(() -> "Failed to send vote for video: " + videoId + " vote: " + vote, ex);
}
+ return false;
}
- private static void confirmVote(String videoId, String userId, String solution) {
+ private static boolean confirmVote(String videoId, String userId, String solution) {
Utils.verifyOffMainThread();
Objects.requireNonNull(videoId);
Objects.requireNonNull(userId);
@@ -359,7 +470,7 @@ public class ReturnYouTubeDislikeApi {
try {
if (checkIfRateLimitInEffect("confirmVote")) {
- return;
+ return false;
}
Logger.printDebug(() -> "Trying to confirm vote for video: " + videoId + " solution: " + solution);
HttpURLConnection connection = getRYDConnectionFromRoute(ReturnYouTubeDislikeRoutes.CONFIRM_VOTE);
@@ -375,25 +486,29 @@ public class ReturnYouTubeDislikeApi {
final int responseCode = connection.getResponseCode();
if (checkIfRateLimitWasHit(responseCode)) {
connection.disconnect(); // disconnect, as no more connections will be made for a little while
- return;
+ return false;
}
if (responseCode == HTTP_STATUS_CODE_SUCCESS) {
Logger.printDebug(() -> "Vote confirm successful for video: " + videoId);
- return;
+ return true;
}
+
// Something went wrong, might as well disconnect.
String response = Requester.parseStringAndDisconnect(connection);
Logger.printInfo(() -> "Failed to confirm vote for video: " + videoId
+ " solution: " + solution + " responseCode: " + responseCode + " response: '" + response + "'");
- handleConnectionError(str("revanced_ryd_failure_connection_status_code", responseCode), null);
+ handleConnectionError(str("revanced_ryd_failure_connection_status_code", responseCode),
+ null, true);
} catch (SocketTimeoutException ex) {
- handleConnectionError(str("revanced_ryd_failure_connection_timeout"), ex);
+ handleConnectionError(str("revanced_ryd_failure_connection_timeout"), ex, false);
} catch (IOException ex) {
- handleConnectionError(str("revanced_ryd_failure_generic", "confirm vote failed"), ex);
+ handleConnectionError(str("revanced_ryd_failure_generic", "confirm vote failed"),
+ ex, true);
} catch (Exception ex) {
Logger.printException(() -> "Failed to confirm vote for video: " + videoId
+ " solution: " + solution, ex); // should never happen
}
+ return false;
}
private static void applyCommonPostRequestSettings(HttpURLConnection connection) throws ProtocolException {
@@ -440,12 +555,12 @@ public class ReturnYouTubeDislikeApi {
}
// https://stackoverflow.com/a/157202
- private static String randomString() {
+ private static String randomString(int len) {
String AB = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
SecureRandom rnd = new SecureRandom();
- StringBuilder sb = new StringBuilder(36);
- for (int i = 0; i < 36; i++)
+ StringBuilder sb = new StringBuilder(len);
+ for (int i = 0; i < len; i++)
sb.append(AB.charAt(rnd.nextInt(AB.length())));
return sb.toString();
}
diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/settings/BaseSettings.java b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/BaseSettings.java
index 973550208..886852d2e 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/shared/settings/BaseSettings.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/BaseSettings.java
@@ -3,7 +3,6 @@ package app.revanced.extension.shared.settings;
import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
import static app.revanced.extension.shared.patches.PatchStatus.HideFullscreenAdsDefaultBoolean;
-import static app.revanced.extension.shared.patches.PatchStatus.SpoofStreamingDataAndroidOnlyDefaultBoolean;
import app.revanced.extension.shared.patches.ReturnYouTubeUsernamePatch.DisplayFormat;
import app.revanced.extension.shared.patches.client.AppClient.ClientType;
@@ -37,9 +36,14 @@ public class BaseSettings {
public static final StringSetting RETURN_YOUTUBE_USERNAME_YOUTUBE_DATA_API_V3_DEVELOPER_KEY = new StringSetting("revanced_return_youtube_username_youtube_data_api_v3_developer_key", "", true, false);
public static final BooleanSetting SPOOF_STREAMING_DATA = new BooleanSetting("revanced_spoof_streaming_data", TRUE, true, "revanced_spoof_streaming_data_user_dialog_message");
- public static final EnumSetting SPOOF_STREAMING_DATA_TYPE = new EnumSetting<>("revanced_spoof_streaming_data_type", ClientType.ANDROID_VR, true);
- public static final BooleanSetting SPOOF_STREAMING_DATA_ANDROID_ONLY = new BooleanSetting("revanced_spoof_streaming_data_android_only", SpoofStreamingDataAndroidOnlyDefaultBoolean(), true, "revanced_spoof_streaming_data_android_only_user_dialog_message");
+ public static final BooleanSetting SPOOF_STREAMING_DATA_IOS_FORCE_AVC = new BooleanSetting("revanced_spoof_streaming_data_ios_force_avc", FALSE, true,
+ "revanced_spoof_streaming_data_ios_force_avc_user_dialog_message");
public static final BooleanSetting SPOOF_STREAMING_DATA_STATS_FOR_NERDS = new BooleanSetting("revanced_spoof_streaming_data_stats_for_nerds", TRUE);
+ // Client type must be last spoof setting due to cyclic references.
+ public static final EnumSetting SPOOF_STREAMING_DATA_TYPE = new EnumSetting<>("revanced_spoof_streaming_data_type", ClientType.ANDROID_VR, true);
+
+ public static final StringSetting SPOOF_STREAMING_DATA_PO_TOKEN = new StringSetting("revanced_spoof_streaming_data_po_token", "", true);
+ public static final StringSetting SPOOF_STREAMING_DATA_VISITOR_DATA = new StringSetting("revanced_spoof_streaming_data_visitor_data", "", true);
/**
* @noinspection DeprecatedIsStillUsed
diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/utils/ResourceUtils.java b/extensions/shared/src/main/java/app/revanced/extension/shared/utils/ResourceUtils.java
index 55b7c1ac6..e3252ef9a 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/shared/utils/ResourceUtils.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/shared/utils/ResourceUtils.java
@@ -1,5 +1,6 @@
package app.revanced.extension.shared.utils;
+import android.app.Activity;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.view.animation.Animation;
@@ -16,12 +17,25 @@ public class ResourceUtils extends Utils {
} // utility class
public static int getIdentifier(@NonNull String str, @NonNull ResourceType resourceType) {
- return getIdentifier(str, resourceType, getContext());
+ Activity mActivity = getActivity();
+ Context mContext = mActivity != null
+ ? mActivity
+ : getContext();
+ if (mContext == null) {
+ handleException(str, resourceType);
+ return 0;
+ }
+ return getIdentifier(str, resourceType, mContext);
}
public static int getIdentifier(@NonNull String str, @NonNull ResourceType resourceType,
@NonNull Context context) {
- return getResources().getIdentifier(str, resourceType.getType(), context.getPackageName());
+ try {
+ return context.getResources().getIdentifier(str, resourceType.getType(), context.getPackageName());
+ } catch (Exception ex) {
+ handleException(str, resourceType);
+ }
+ return 0;
}
public static int getAnimIdentifier(@NonNull String str) {
diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/utils/StringRef.java b/extensions/shared/src/main/java/app/revanced/extension/shared/utils/StringRef.java
index f51b49ed0..902e354e2 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/shared/utils/StringRef.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/shared/utils/StringRef.java
@@ -1,6 +1,7 @@
package app.revanced.extension.shared.utils;
import android.annotation.SuppressLint;
+import android.app.Activity;
import android.content.Context;
import android.content.res.Resources;
@@ -104,7 +105,10 @@ public class StringRef extends Utils {
public String toString() {
if (!resolved) {
try {
- Context context = getContext();
+ Activity mActivity = getActivity();
+ Context context = mActivity != null
+ ? mActivity
+ : getContext();
if (resources == null) {
resources = getResources();
}
diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/utils/Utils.java b/extensions/shared/src/main/java/app/revanced/extension/shared/utils/Utils.java
index d8e6a0afc..ed3e96428 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/shared/utils/Utils.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/shared/utils/Utils.java
@@ -54,8 +54,6 @@ public class Utils {
@SuppressLint("StaticFieldLeak")
public static Context context;
- private static Resources resources;
-
protected Utils() {
} // utility class
@@ -142,6 +140,24 @@ public class Utils {
return backgroundThreadPool.submit(call);
}
+ /**
+ * Simulates a delay by doing meaningless calculations.
+ * Used for debugging to verify UI timeout logic.
+ */
+ @SuppressWarnings("UnusedReturnValue")
+ public static long doNothingForDuration(long amountOfTimeToWaste) {
+ final long timeCalculationStarted = System.currentTimeMillis();
+ Logger.printDebug(() -> "Artificially creating delay of: " + amountOfTimeToWaste + "ms");
+
+ long meaninglessValue = 0;
+ while (System.currentTimeMillis() - timeCalculationStarted < amountOfTimeToWaste) {
+ // could do a thread sleep, but that will trigger an exception if the thread is interrupted
+ meaninglessValue += Long.numberOfLeadingZeros((long) Math.exp(Math.random()));
+ }
+ // return the value, otherwise the compiler or VM might optimize and remove the meaningless time wasting work,
+ // leaving an empty loop that hammers on the System.currentTimeMillis native call
+ return meaninglessValue;
+ }
public static boolean containsAny(@NonNull String value, @NonNull String... targets) {
return indexOfFirstFound(value, targets) >= 0;
@@ -264,11 +280,15 @@ public class Utils {
}
public static Resources getResources() {
- if (resources == null) {
- return getLocalizedContextAndSetResources(getContext()).getResources();
- } else {
- return resources;
+ Activity mActivity = activityRef.get();
+ if (mActivity != null) {
+ return mActivity.getResources();
}
+ Context mContext = getContext();
+ if (mContext != null) {
+ return mContext.getResources();
+ }
+ throw new IllegalStateException("Get resources failed");
}
/**
@@ -286,6 +306,9 @@ public class Utils {
if (mActivity == null) {
return mContext;
}
+ if (mContext == null) {
+ return null;
+ }
// Locale of MainActivity.
Locale applicationLocale;
@@ -303,7 +326,6 @@ public class Utils {
// If they are identical, no need to override them.
if (applicationLocale == contextLocale) {
- resources = mActivity.getResources();
return mContext;
}
@@ -311,9 +333,7 @@ public class Utils {
Locale.setDefault(applicationLocale);
Configuration configuration = new Configuration(mContext.getResources().getConfiguration());
configuration.setLocale(applicationLocale);
- Context localizedContext = mContext.createConfigurationContext(configuration);
- resources = localizedContext.getResources();
- return localizedContext;
+ return mContext.createConfigurationContext(configuration);
}
public static void setActivity(Activity mainActivity) {
@@ -462,16 +482,6 @@ public class Utils {
return false;
}
- public static boolean isDarkModeEnabled() {
- return isDarkModeEnabled(context);
- }
-
- public static boolean isDarkModeEnabled(Context context) {
- Configuration config = context.getResources().getConfiguration();
- final int currentNightMode = config.uiMode & Configuration.UI_MODE_NIGHT_MASK;
- return currentNightMode == Configuration.UI_MODE_NIGHT_YES;
- }
-
/**
* @return whether the device's API level is higher than a specific SDK version.
*/
@@ -514,6 +524,11 @@ public class Utils {
);
}
+ public static boolean isLandscapeOrientation() {
+ final int orientation = context.getResources().getConfiguration().orientation;
+ return orientation == Configuration.ORIENTATION_LANDSCAPE;
+ }
+
/**
* Automatically logs any exceptions the runnable throws.
*
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/CarouselShelfFilter.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/CarouselShelfFilter.java
index 4dd9ace46..e565fe92c 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/CarouselShelfFilter.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/CarouselShelfFilter.java
@@ -18,6 +18,7 @@ public final class CarouselShelfFilter extends Filter {
private static final String BROWSE_ID_CLIP = "FEclips";
private static final String BROWSE_ID_HOME = "FEwhat_to_watch";
private static final String BROWSE_ID_LIBRARY = "FElibrary";
+ private static final String BROWSE_ID_MOVIE = "FEstorefront";
private static final String BROWSE_ID_NOTIFICATION = "FEactivity";
private static final String BROWSE_ID_NOTIFICATION_INBOX = "FEnotifications_inbox";
private static final String BROWSE_ID_PLAYLIST = "VLPL";
@@ -32,9 +33,10 @@ public final class CarouselShelfFilter extends Filter {
);
private static final Supplier> whitelistBrowseId = () -> Stream.of(
- BROWSE_ID_LIBRARY,
- BROWSE_ID_NOTIFICATION_INBOX,
BROWSE_ID_CLIP,
+ BROWSE_ID_LIBRARY,
+ BROWSE_ID_MOVIE,
+ BROWSE_ID_NOTIFICATION_INBOX,
BROWSE_ID_PREMIUM
);
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/ChangeStartPagePatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/ChangeStartPagePatch.java
index 272eac1dd..759397604 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/ChangeStartPagePatch.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/ChangeStartPagePatch.java
@@ -32,12 +32,15 @@ public final class ChangeStartPagePatch {
HISTORY("FEhistory", TRUE),
LIBRARY("FElibrary", TRUE),
MOVIE("FEstorefront", TRUE),
+ NOTIFICATIONS("FEactivity", TRUE),
SUBSCRIPTIONS("FEsubscriptions", TRUE),
TRENDING("FEtrending", TRUE),
+ YOUR_CLIPS("FEclips", TRUE),
/**
* Channel id, this can be used as a browseId.
*/
+ COURSES("UCtFRv9O2AHqOZjjynzrv-xg", TRUE),
GAMING("UCOpNcN46UbXVtpKMrmU4Abg", TRUE),
LIVE("UC4R8DWoMoI7CAwX8_LjQHig", TRUE),
MUSIC("UC-9-kyTW8ZkZNDHQJ6FgpwQ", TRUE),
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/GeneralPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/GeneralPatch.java
index 0893398fb..1223d4115 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/GeneralPatch.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/GeneralPatch.java
@@ -5,7 +5,6 @@ import static app.revanced.extension.shared.utils.Utils.getChildView;
import static app.revanced.extension.shared.utils.Utils.hideViewByLayoutParams;
import static app.revanced.extension.shared.utils.Utils.hideViewGroupByMarginLayoutParams;
import static app.revanced.extension.shared.utils.Utils.hideViewUnderCondition;
-import static app.revanced.extension.shared.utils.Utils.isSDKAbove;
import static app.revanced.extension.youtube.patches.utils.PatchStatus.ImageSearchButton;
import static app.revanced.extension.youtube.shared.NavigationBar.NavigationButton;
@@ -41,10 +40,10 @@ import java.util.EnumMap;
import java.util.Map;
import java.util.Objects;
-import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.ResourceUtils;
import app.revanced.extension.shared.utils.Utils;
import app.revanced.extension.youtube.settings.Settings;
+import app.revanced.extension.youtube.utils.ExtendedUtils;
import app.revanced.extension.youtube.utils.ThemeUtils;
@SuppressWarnings("unused")
@@ -109,12 +108,7 @@ public class GeneralPatch {
// region [Disable splash animation] patch
public static boolean disableSplashAnimation(boolean original) {
- try {
- return !Settings.DISABLE_SPLASH_ANIMATION.get() && original;
- } catch (Exception ex) {
- Logger.printException(() -> "Failed to load disableSplashAnimation", ex);
- }
- return original;
+ return !Settings.DISABLE_SPLASH_ANIMATION.get() && original;
}
// endregion
@@ -129,6 +123,10 @@ public class GeneralPatch {
// region [Hide layout components] patch
+ public static boolean disableTranslucentStatusBar(boolean original) {
+ return !Settings.DISABLE_TRANSLUCENT_STATUS_BAR.get() && original;
+ }
+
private static String[] accountMenuBlockList;
static {
@@ -198,8 +196,6 @@ public class GeneralPatch {
// region [Hide navigation bar components] patch
- private static final int fillBellCairoBlack = ResourceUtils.getDrawableIdentifier("yt_fill_bell_cairo_black_24");
-
private static final Map shouldHideMap = new EnumMap<>(NavigationButton.class) {
{
put(NavigationButton.HOME, Settings.HIDE_NAVIGATION_HOME_BUTTON.get());
@@ -215,10 +211,15 @@ public class GeneralPatch {
return Settings.ENABLE_NARROW_NAVIGATION_BUTTONS.get() || original;
}
+ public static boolean enableTranslucentNavigationBar() {
+ return Settings.ENABLE_TRANSLUCENT_NAVIGATION_BAR.get();
+ }
+
/**
* @noinspection ALL
*/
public static void setCairoNotificationFilledIcon(EnumMap enumMap, Enum tabActivityCairo) {
+ final int fillBellCairoBlack = ResourceUtils.getDrawableIdentifier("yt_fill_bell_cairo_black_24");
if (fillBellCairoBlack != 0) {
// It's very unlikely, but Google might fix this issue someday.
// If so, [fillBellCairoBlack] might already be in enumMap.
@@ -245,48 +246,6 @@ public class GeneralPatch {
hideViewUnderCondition(Settings.HIDE_NAVIGATION_BAR.get(), view);
}
- public static boolean useTranslucentNavigationStatusBar(boolean original) {
- try {
- if (Settings.DISABLE_TRANSLUCENT_STATUS_BAR.get()) {
- return false;
- }
- } catch (Exception ex) {
- Logger.printException(() -> "Failed to load useTranslucentNavigationStatusBar", ex);
- }
-
- return original;
- }
-
- private static final Boolean DISABLE_TRANSLUCENT_NAVIGATION_BAR_LIGHT
- = Settings.DISABLE_TRANSLUCENT_NAVIGATION_BAR_LIGHT.get();
-
- private static final Boolean DISABLE_TRANSLUCENT_NAVIGATION_BAR_DARK
- = Settings.DISABLE_TRANSLUCENT_NAVIGATION_BAR_DARK.get();
-
- public static boolean useTranslucentNavigationButtons(boolean original) {
- try {
- // Feature requires Android 13+
- if (!isSDKAbove(33)) {
- return original;
- }
-
- if (!DISABLE_TRANSLUCENT_NAVIGATION_BAR_DARK && !DISABLE_TRANSLUCENT_NAVIGATION_BAR_LIGHT) {
- return original;
- }
-
- if (DISABLE_TRANSLUCENT_NAVIGATION_BAR_DARK && DISABLE_TRANSLUCENT_NAVIGATION_BAR_LIGHT) {
- return false;
- }
-
- return Utils.isDarkModeEnabled()
- ? !DISABLE_TRANSLUCENT_NAVIGATION_BAR_DARK
- : !DISABLE_TRANSLUCENT_NAVIGATION_BAR_LIGHT;
- } catch (Exception ex) {
- Logger.printException(() -> "Failed to load useTranslucentNavigationButtons", ex);
- }
- return original;
- }
-
// endregion
// region [Remove viewer discretion dialog] patch
@@ -428,6 +387,7 @@ public class GeneralPatch {
public static void setWideSearchBarLayout(View view) {
if (!wideSearchbarEnabled)
return;
+
if (!(view.findViewById(searchBarId) instanceof RelativeLayout searchBarView))
return;
@@ -550,12 +510,27 @@ public class GeneralPatch {
private static final int settingsDrawableId =
ResourceUtils.getDrawableIdentifier("yt_outline_gear_black_24");
+ private static final int settingsCairoDrawableId =
+ ResourceUtils.getDrawableIdentifier("yt_outline_gear_cairo_black_24");
public static int getCreateButtonDrawableId(int original) {
- return Settings.REPLACE_TOOLBAR_CREATE_BUTTON.get() &&
- settingsDrawableId != 0
+ if (!Settings.REPLACE_TOOLBAR_CREATE_BUTTON.get()) {
+ return original;
+ }
+
+ if (settingsDrawableId == 0) {
+ return original;
+ }
+
+ // If the user has patched YouTube 19.26.42,
+ // Or spoofed the app version to 19.26.42 or earlier.
+ if (!ExtendedUtils.IS_19_28_OR_GREATER || ExtendedUtils.isSpoofingToLessThan("19.27.00")) {
+ return settingsDrawableId;
+ }
+
+ return settingsCairoDrawableId == 0
? settingsDrawableId
- : original;
+ : settingsCairoDrawableId;
}
public static void replaceCreateButton(String enumString, View toolbarView) {
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/MiniplayerPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/MiniplayerPatch.java
index b231f24d9..615d27e10 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/MiniplayerPatch.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/MiniplayerPatch.java
@@ -12,6 +12,8 @@ import static app.revanced.extension.youtube.utils.ExtendedUtils.IS_19_26_OR_GRE
import static app.revanced.extension.youtube.utils.ExtendedUtils.IS_19_29_OR_GREATER;
import static app.revanced.extension.youtube.utils.ExtendedUtils.validateValue;
+import android.content.Context;
+import android.content.res.Resources;
import android.util.DisplayMetrics;
import android.view.View;
import android.view.ViewGroup;
@@ -82,49 +84,56 @@ public final class MiniplayerPatch {
}
}
- private static final int MINIPLAYER_SIZE;
+ private static int MINIPLAYER_SIZE = 0;
static {
- // YT appears to use the device screen dip width, plus an unknown fixed horizontal padding size.
- DisplayMetrics displayMetrics = Utils.getContext().getResources().getDisplayMetrics();
- final int deviceDipWidth = (int) (displayMetrics.widthPixels / displayMetrics.density);
+ setMiniPlayerSize();
+ }
- // YT seems to use a minimum height to calculate the minimum miniplayer width based on the video.
- // 170 seems to be the smallest that can be used and using less makes no difference.
- final int WIDTH_DIP_MIN = 170; // Seems to be the smallest that works.
- final int HORIZONTAL_PADDING_DIP = 15; // Estimated padding.
- // Round down to the nearest 5 pixels, to keep any error toasts easier to read.
- final int estimatedWidthDipMax = 5 * ((deviceDipWidth - HORIZONTAL_PADDING_DIP) / 5);
- // On some ultra low end devices the pixel width and density are the same number,
- // which causes the estimate to always give a value of 1.
- // Fix this by using a fixed size of double the min width.
- final int WIDTH_DIP_MAX = estimatedWidthDipMax <= WIDTH_DIP_MIN
- ? 2 * WIDTH_DIP_MIN
- : estimatedWidthDipMax;
- Logger.printDebug(() -> "Screen dip width: " + deviceDipWidth + " maxWidth: " + WIDTH_DIP_MAX);
+ private static void setMiniPlayerSize() {
+ try {
+ Context context = Utils.getContext();
+ if (context == null) {
+ return;
+ }
+ Resources resources = context.getResources();
+ if (resources == null) {
+ return;
+ }
+ // YT appears to use the device screen dip width, plus an unknown fixed horizontal padding size.
+ DisplayMetrics displayMetrics = resources.getDisplayMetrics();
+ final int deviceDipWidth = (int) (displayMetrics.widthPixels / displayMetrics.density);
- int dipWidth = Settings.MINIPLAYER_WIDTH_DIP.get();
+ // YT seems to use a minimum height to calculate the minimum miniplayer width based on the video.
+ // 170 seems to be the smallest that can be used and using less makes no difference.
+ final int WIDTH_DIP_MIN = 170; // Seems to be the smallest that works.
+ final int HORIZONTAL_PADDING_DIP = 15; // Estimated padding.
+ // Round down to the nearest 5 pixels, to keep any error toasts easier to read.
+ final int estimatedWidthDipMax = 5 * ((deviceDipWidth - HORIZONTAL_PADDING_DIP) / 5);
+ // On some ultra low end devices the pixel width and density are the same number,
+ // which causes the estimate to always give a value of 1.
+ // Fix this by using a fixed size of double the min width.
+ final int WIDTH_DIP_MAX = estimatedWidthDipMax <= WIDTH_DIP_MIN
+ ? 2 * WIDTH_DIP_MIN
+ : estimatedWidthDipMax;
+ Logger.printDebug(() -> "Screen dip width: " + deviceDipWidth + " maxWidth: " + WIDTH_DIP_MAX);
- if (dipWidth < WIDTH_DIP_MIN || dipWidth > WIDTH_DIP_MAX) {
- Utils.showToastShort(str("revanced_miniplayer_width_dip_invalid_toast",
- WIDTH_DIP_MIN, WIDTH_DIP_MAX));
- Utils.showToastShort(str("revanced_extended_reset_to_default_toast"));
+ int dipWidth = Settings.MINIPLAYER_WIDTH_DIP.get();
- // Instead of resetting, clamp the size at the bounds.
- dipWidth = Math.max(WIDTH_DIP_MIN, Math.min(dipWidth, WIDTH_DIP_MAX));
- Settings.MINIPLAYER_WIDTH_DIP.save(dipWidth);
+ if (dipWidth < WIDTH_DIP_MIN || dipWidth > WIDTH_DIP_MAX) {
+ Utils.showToastShort(str("revanced_miniplayer_width_dip_invalid_toast",
+ WIDTH_DIP_MIN, WIDTH_DIP_MAX));
+ Utils.showToastShort(str("revanced_extended_reset_to_default_toast"));
+
+ // Instead of resetting, clamp the size at the bounds.
+ dipWidth = Math.max(WIDTH_DIP_MIN, Math.min(dipWidth, WIDTH_DIP_MAX));
+ Settings.MINIPLAYER_WIDTH_DIP.save(dipWidth);
+ }
+
+ MINIPLAYER_SIZE = dipWidth;
+ } catch (Exception ex) {
+ Logger.printException(() -> "setMiniPlayerSize failure", ex);
}
-
- MINIPLAYER_SIZE = dipWidth;
-
- final int opacity = validateValue(
- Settings.MINIPLAYER_OPACITY,
- 0,
- 100,
- "revanced_miniplayer_opacity_invalid_toast"
- );
-
- OPACITY_LEVEL = (opacity * 255) / 100;
}
/**
@@ -175,6 +184,17 @@ public final class MiniplayerPatch {
private static final int OPACITY_LEVEL;
+ static {
+ final int opacity = validateValue(
+ Settings.MINIPLAYER_OPACITY,
+ 0,
+ 100,
+ "revanced_miniplayer_opacity_invalid_toast"
+ );
+
+ OPACITY_LEVEL = (opacity * 255) / 100;
+ }
+
public static final class MiniplayerHorizontalDragAvailability implements Setting.Availability {
@Override
public boolean isAvailable() {
@@ -293,7 +313,12 @@ public final class MiniplayerPatch {
*/
public static int setMiniplayerDefaultSize(int original) {
if (CURRENT_TYPE.isModern()) {
- return MINIPLAYER_SIZE;
+ if (MINIPLAYER_SIZE == 0) {
+ setMiniPlayerSize();
+ }
+ if (MINIPLAYER_SIZE != 0) {
+ return MINIPLAYER_SIZE;
+ }
}
return original;
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/player/EnterFullscreenPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/player/EnterFullscreenPatch.java
new file mode 100644
index 000000000..5c5bed7ab
--- /dev/null
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/player/EnterFullscreenPatch.java
@@ -0,0 +1,67 @@
+package app.revanced.extension.youtube.patches.player;
+
+import androidx.annotation.NonNull;
+
+import app.revanced.extension.shared.utils.Logger;
+import app.revanced.extension.shared.utils.Utils;
+import app.revanced.extension.youtube.settings.Settings;
+import app.revanced.extension.youtube.shared.PlayerType;
+import app.revanced.extension.youtube.utils.VideoUtils;
+
+@SuppressWarnings("unused")
+public class EnterFullscreenPatch {
+ private static volatile boolean isForeground = true;
+
+ @NonNull
+ private static String videoId = "";
+
+ /**
+ * Injection point.
+ */
+ public static void onAppBackgrounded() {
+ isForeground = false;
+ }
+
+ /**
+ * Injection point.
+ */
+ public static void onAppForegrounded() {
+ isForeground = true;
+ }
+
+ /**
+ * Injection point.
+ */
+ public static void enterFullscreen(@NonNull String newlyLoadedChannelId, @NonNull String newlyLoadedChannelName,
+ @NonNull String newlyLoadedVideoId, @NonNull String newlyLoadedVideoTitle,
+ final long newlyLoadedVideoLength, boolean newlyLoadedLiveStreamValue) {
+ try {
+ if (!Settings.ENTER_FULLSCREEN.get()) {
+ return;
+ }
+ PlayerType playerType = PlayerType.getCurrent();
+ // 1. The user opened the video while playing a video in the feed.
+ // 2. This is a valid request, so the videoId is not saved.
+ if (playerType == PlayerType.INLINE_MINIMAL) {
+ return;
+ }
+ if (videoId.equals(newlyLoadedVideoId)) {
+ return;
+ }
+ videoId = newlyLoadedVideoId;
+
+ // 1. User clicks home button in [PlayerType.WATCH_WHILE_MAXIMIZED], thus entering audio only mode.
+ // 2. PlayerType is still [PlayerType.WATCH_WHILE_MAXIMIZED].
+ // 3. Next video starts in audio only mode, then returns to foreground mode.
+ // 4. Enters fullscreen for a moment and then returns.
+ // We can prevent this by checking if the app is in the foreground.
+ if (playerType == PlayerType.WATCH_WHILE_MAXIMIZED && isForeground) {
+ // It works without delay, but in this case sometimes portrait videos have landscape orientation.
+ Utils.runOnMainThreadDelayed(VideoUtils::enterFullscreenMode, 250L);
+ }
+ } catch (Exception ex) {
+ Logger.printException(() -> "enterFullscreen failure", ex);
+ }
+ }
+
+}
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/player/ExitFullscreenPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/player/ExitFullscreenPatch.java
new file mode 100644
index 000000000..c66afb75d
--- /dev/null
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/player/ExitFullscreenPatch.java
@@ -0,0 +1,46 @@
+package app.revanced.extension.youtube.patches.player;
+
+import app.revanced.extension.shared.utils.Logger;
+import app.revanced.extension.shared.utils.Utils;
+import app.revanced.extension.youtube.settings.Settings;
+import app.revanced.extension.youtube.shared.PlayerType;
+import app.revanced.extension.youtube.utils.VideoUtils;
+
+@SuppressWarnings("unused")
+public class ExitFullscreenPatch {
+
+ public enum FullscreenMode {
+ DISABLED,
+ PORTRAIT,
+ LANDSCAPE,
+ PORTRAIT_LANDSCAPE,
+ }
+
+ /**
+ * Injection point.
+ */
+ public static void endOfVideoReached() {
+ try {
+ FullscreenMode mode = Settings.EXIT_FULLSCREEN.get();
+ if (mode == FullscreenMode.DISABLED) {
+ return;
+ }
+
+ if (PlayerType.getCurrent() == PlayerType.WATCH_WHILE_FULLSCREEN) {
+ if (mode != FullscreenMode.PORTRAIT_LANDSCAPE) {
+ if (Utils.isLandscapeOrientation()) {
+ if (mode == FullscreenMode.PORTRAIT) {
+ return;
+ }
+ } else if (mode == FullscreenMode.LANDSCAPE) {
+ return;
+ }
+ }
+
+ Utils.runOnMainThread(VideoUtils::exitFullscreenMode);
+ }
+ } catch (Exception ex) {
+ Logger.printException(() -> "endOfVideoReached failure", ex);
+ }
+ }
+}
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/player/PlayerPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/player/PlayerPatch.java
index e7fd135d1..669d57fae 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/player/PlayerPatch.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/player/PlayerPatch.java
@@ -5,8 +5,6 @@ import static app.revanced.extension.shared.utils.Utils.hideViewByRemovingFromPa
import static app.revanced.extension.shared.utils.Utils.hideViewUnderCondition;
import static app.revanced.extension.youtube.utils.ExtendedUtils.validateValue;
-import android.app.Activity;
-import android.content.pm.ActivityInfo;
import android.support.v7.widget.RecyclerView;
import android.util.TypedValue;
import android.view.View;
@@ -20,7 +18,6 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
-import java.lang.ref.WeakReference;
import java.util.Objects;
import app.revanced.extension.shared.settings.BaseSettings;
@@ -328,37 +325,6 @@ public class PlayerPatch {
Utils.runOnMainThreadDelayed(() -> isScreenOn = false, Settings.KEEP_LANDSCAPE_MODE_TIMEOUT.get());
}
- private static WeakReference watchDescriptorActivityRef = new WeakReference<>(null);
- private static volatile boolean isLandScapeVideo = true;
-
- public static void setWatchDescriptorActivity(Activity activity) {
- watchDescriptorActivityRef = new WeakReference<>(activity);
- }
-
- public static boolean forceFullscreen(boolean original) {
- if (!Settings.FORCE_FULLSCREEN.get())
- return original;
-
- Utils.runOnMainThreadDelayed(PlayerPatch::setOrientation, 1000);
- return true;
- }
-
- private static void setOrientation() {
- final Activity watchDescriptorActivity = watchDescriptorActivityRef.get();
- final int requestedOrientation = isLandScapeVideo
- ? ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
- : watchDescriptorActivity.getRequestedOrientation();
-
- watchDescriptorActivity.setRequestedOrientation(requestedOrientation);
- }
-
- public static void setVideoPortrait(int width, int height) {
- if (!Settings.FORCE_FULLSCREEN.get())
- return;
-
- isLandScapeVideo = width > height;
- }
-
// endregion
// region [Hide comments component] patch
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/swipe/SwipeControlsPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/swipe/SwipeControlsPatch.java
index 01b302675..f4b0d16ad 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/swipe/SwipeControlsPatch.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/swipe/SwipeControlsPatch.java
@@ -27,8 +27,22 @@ public class SwipeControlsPatch {
/**
* Injection point.
*/
- public static boolean disableWatchPanelGestures() {
- return !Settings.DISABLE_WATCH_PANEL_GESTURES.get();
+ public static boolean disableSwipeToEnterFullscreenModeBelowThePlayer() {
+ return !Settings.DISABLE_SWIPE_TO_ENTER_FULLSCREEN_MODE_BELOW_THE_PLAYER.get();
+ }
+
+ /**
+ * Injection point.
+ */
+ public static boolean disableSwipeToEnterFullscreenModeInThePlayer(boolean original) {
+ return !Settings.DISABLE_SWIPE_TO_ENTER_FULLSCREEN_MODE_IN_THE_PLAYER.get() && original;
+ }
+
+ /**
+ * Injection point.
+ */
+ public static boolean disableSwipeToExitFullscreenMode(boolean original) {
+ return !Settings.DISABLE_SWIPE_TO_EXIT_FULLSCREEN_MODE.get() && original;
}
/**
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/PatchStatus.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/PatchStatus.java
index 309415c0d..10fefa0e2 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/PatchStatus.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/PatchStatus.java
@@ -7,11 +7,6 @@ public class PatchStatus {
return false;
}
- public static boolean MinimalHeader() {
- // Replace this with true If the Custom header patch succeeds and the patch option was `youtube_minimal_header`
- return false;
- }
-
public static boolean PlayerButtons() {
// Replace this with true if the Hide player buttons patch succeeds
return false;
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/PlayerControlsPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/PlayerControlsPatch.java
index 5a6e56f6a..7a3442505 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/PlayerControlsPatch.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/PlayerControlsPatch.java
@@ -106,7 +106,7 @@ public class PlayerControlsPatch {
}
}
- private static void changeVisibilityNegatedImmediately() {
+ public static void changeVisibilityNegatedImmediately() {
// AlwaysRepeat.changeVisibilityNegatedImmediate();
// CopyVideoUrl.changeVisibilityNegatedImmediate();
// CopyVideoUrlTimestamp.changeVisibilityNegatedImmediate();
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/ReturnYouTubeDislikePatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/ReturnYouTubeDislikePatch.java
index 8e29174e5..4c94eb6a4 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/ReturnYouTubeDislikePatch.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/ReturnYouTubeDislikePatch.java
@@ -360,8 +360,8 @@ public class ReturnYouTubeDislikePatch {
removeRollingNumberPatchChanges(view);
return original;
}
- final boolean isDescriptionPanel = view.getParent() instanceof ViewGroup viewGroupParent
- && viewGroupParent.getChildCount() < 2;
+ final boolean isDescriptionPanel = view.getParent() instanceof ViewGroup viewGroupParent &&
+ viewGroupParent.getChildCount() < 2;
// Called for all instances of RollingNumber, so must check if text is for a dislikes.
// Text will already have the correct content but it's missing the drawable separators.
if (!ReturnYouTubeDislike.isPreviouslyCreatedSegmentedSpan(original.toString()) || isDescriptionPanel) {
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/PlaybackSpeedPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/PlaybackSpeedPatch.java
index ca2ad8fc1..5211c6a47 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/PlaybackSpeedPatch.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/PlaybackSpeedPatch.java
@@ -10,13 +10,15 @@ import org.apache.commons.lang3.BooleanUtils;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.Utils;
import app.revanced.extension.youtube.patches.utils.PatchStatus;
-import app.revanced.extension.youtube.patches.video.requests.PlaylistRequest;
+import app.revanced.extension.youtube.patches.video.requests.MusicRequest;
import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.shared.VideoInformation;
import app.revanced.extension.youtube.whitelist.Whitelist;
@SuppressWarnings("unused")
public class PlaybackSpeedPatch {
+ private static final boolean DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC =
+ Settings.DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC.get();
private static final long TOAST_DELAY_MILLISECONDS = 750;
private static long lastTimeSpeedChanged;
private static boolean isLiveStream;
@@ -39,8 +41,8 @@ public class PlaybackSpeedPatch {
/**
* Injection point.
*/
- public static void fetchPlaylistData(@NonNull String videoId, boolean isShortAndOpeningOrPlaying) {
- if (Settings.DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC.get()) {
+ public static void fetchMusicRequest(@NonNull String videoId, boolean isShortAndOpeningOrPlaying) {
+ if (DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC) {
try {
final boolean videoIdIsShort = VideoInformation.lastPlayerResponseIsShort();
// Shorts shelf in home and subscription feed causes player response hook to be called,
@@ -50,9 +52,12 @@ public class PlaybackSpeedPatch {
return;
}
- PlaylistRequest.fetchRequestIfNeeded(videoId);
+ MusicRequest.fetchRequestIfNeeded(
+ videoId,
+ Settings.DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC_TYPE.get()
+ );
} catch (Exception ex) {
- Logger.printException(() -> "fetchPlaylistData failure", ex);
+ Logger.printException(() -> "fetchMusicRequest failure", ex);
}
}
}
@@ -61,15 +66,16 @@ public class PlaybackSpeedPatch {
* Injection point.
*/
public static float getPlaybackSpeedInShorts(final float playbackSpeed) {
- if (!VideoInformation.lastPlayerResponseIsShort())
- return playbackSpeed;
- if (!Settings.ENABLE_DEFAULT_PLAYBACK_SPEED_SHORTS.get())
- return playbackSpeed;
+ if (VideoInformation.lastPlayerResponseIsShort() &&
+ Settings.ENABLE_DEFAULT_PLAYBACK_SPEED_SHORTS.get()
+ ) {
+ float defaultPlaybackSpeed = getDefaultPlaybackSpeed(VideoInformation.getChannelId(), null);
+ Logger.printDebug(() -> "overridePlaybackSpeed in Shorts: " + defaultPlaybackSpeed);
- float defaultPlaybackSpeed = getDefaultPlaybackSpeed(VideoInformation.getChannelId(), null);
- Logger.printDebug(() -> "overridePlaybackSpeed in Shorts: " + defaultPlaybackSpeed);
+ return defaultPlaybackSpeed;
+ }
- return defaultPlaybackSpeed;
+ return playbackSpeed;
}
/**
@@ -118,23 +124,21 @@ public class PlaybackSpeedPatch {
}
private static float getDefaultPlaybackSpeed(@NonNull String channelId, @Nullable String videoId) {
- return (Settings.DISABLE_DEFAULT_PLAYBACK_SPEED_LIVE.get() && isLiveStream) ||
- Whitelist.isChannelWhitelistedPlaybackSpeed(channelId) ||
- getPlaylistData(videoId)
+ return (isLiveStream || Whitelist.isChannelWhitelistedPlaybackSpeed(channelId) || isMusic(videoId))
? 1.0f
: Settings.DEFAULT_PLAYBACK_SPEED.get();
}
- private static boolean getPlaylistData(@Nullable String videoId) {
- if (Settings.DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC.get() && videoId != null) {
+ private static boolean isMusic(@Nullable String videoId) {
+ if (DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC && videoId != null) {
try {
- PlaylistRequest request = PlaylistRequest.getRequestForVideoId(videoId);
- final boolean isPlaylist = request != null && BooleanUtils.toBoolean(request.getStream());
- Logger.printDebug(() -> "isPlaylist: " + isPlaylist);
+ MusicRequest request = MusicRequest.getRequestForVideoId(videoId);
+ final boolean isMusic = request != null && BooleanUtils.toBoolean(request.getStream());
+ Logger.printDebug(() -> "videoId: " + videoId + ", isMusic: " + isMusic);
- return isPlaylist;
+ return isMusic;
} catch (Exception ex) {
- Logger.printException(() -> "getPlaylistData failure", ex);
+ Logger.printException(() -> "getMusicRequest failure", ex);
}
}
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/VideoQualityPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/VideoQualityPatch.java
index c42125c0d..d13e2e354 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/VideoQualityPatch.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/VideoQualityPatch.java
@@ -38,7 +38,7 @@ public class VideoQualityPatch {
if (videoId.equals(newlyLoadedVideoId))
return;
videoId = newlyLoadedVideoId;
- setVideoQuality(Settings.SKIP_PRELOADED_BUFFER.get() ? 250 : 500);
+ setVideoQuality(Settings.SKIP_PRELOADED_BUFFER.get() ? 250 : 750);
}
/**
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/requests/MusicRequest.kt b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/requests/MusicRequest.kt
new file mode 100644
index 000000000..8299757fc
--- /dev/null
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/requests/MusicRequest.kt
@@ -0,0 +1,249 @@
+package app.revanced.extension.youtube.patches.video.requests
+
+import android.annotation.SuppressLint
+import androidx.annotation.GuardedBy
+import app.revanced.extension.shared.patches.client.AppClient
+import app.revanced.extension.shared.patches.client.WebClient
+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.shared.VideoInformation
+import org.json.JSONException
+import org.json.JSONObject
+import java.io.IOException
+import java.net.SocketTimeoutException
+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 MusicRequest private constructor(
+ private val videoId: String,
+ private val checkCategory: Boolean
+) {
+ /**
+ * Time this instance and the fetch future was created.
+ */
+ private val timeFetched = System.currentTimeMillis()
+ private val future: Future = Utils.submitOnBackgroundThread {
+ fetch(
+ videoId,
+ checkCategory,
+ )
+ }
+
+ fun isExpired(now: Long): Boolean {
+ val timeSinceCreation = now - timeFetched
+ if (timeSinceCreation > CACHE_RETENTION_TIME_MILLISECONDS) {
+ return true
+ }
+
+ // Only expired if the fetch failed (API null response).
+ return (fetchCompleted() && stream == null)
+ }
+
+ /**
+ * @return if the fetch call has completed.
+ */
+ private fun fetchCompleted(): Boolean {
+ return future.isDone
+ }
+
+ val stream: Boolean?
+ get() {
+ try {
+ return future[MAX_MILLISECONDS_TO_WAIT_FOR_FETCH, TimeUnit.MILLISECONDS]
+ } catch (ex: TimeoutException) {
+ Logger.printInfo(
+ { "getStream timed out" },
+ ex
+ )
+ } catch (ex: InterruptedException) {
+ Logger.printException(
+ { "getStream interrupted" },
+ ex
+ )
+ Thread.currentThread().interrupt() // Restore interrupt status flag.
+ } catch (ex: ExecutionException) {
+ Logger.printException(
+ { "getStream failure" },
+ ex
+ )
+ }
+
+ return null
+ }
+
+ companion object {
+ /**
+ * How long to keep fetches until they are expired.
+ */
+ private const val CACHE_RETENTION_TIME_MILLISECONDS = 60 * 1000L // 1 Minute
+
+ private const val MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000L // 20 seconds
+
+ @GuardedBy("itself")
+ private val cache: MutableMap = HashMap()
+
+ @JvmStatic
+ @SuppressLint("ObsoleteSdkInt")
+ fun fetchRequestIfNeeded(videoId: String, checkCategory: Boolean) {
+ Objects.requireNonNull(videoId)
+ synchronized(cache) {
+ val now = System.currentTimeMillis()
+ cache.values.removeIf { request: MusicRequest ->
+ val expired = request.isExpired(now)
+ if (expired) Logger.printDebug { "Removing expired stream: " + request.videoId }
+ expired
+ }
+ if (!cache.containsKey(videoId)) {
+ cache[videoId] = MusicRequest(videoId, checkCategory)
+ }
+ }
+ }
+
+ @JvmStatic
+ fun getRequestForVideoId(videoId: String): MusicRequest? {
+ synchronized(cache) {
+ return cache[videoId]
+ }
+ }
+
+ private fun handleConnectionError(toastMessage: String, ex: Exception?) {
+ Logger.printInfo({ toastMessage }, ex)
+ }
+
+ private fun sendApplicationRequest(videoId: String): JSONObject? {
+ Objects.requireNonNull(videoId)
+
+ val startTime = System.currentTimeMillis()
+ val clientType = AppClient.ClientType.ANDROID_VR
+ val clientTypeName = clientType.name
+ Logger.printDebug { "Fetching playlist request for: $videoId using client: $clientTypeName" }
+
+ try {
+ val connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(
+ PlayerRoutes.GET_PLAYLIST_PAGE,
+ clientType
+ )
+ val requestBody =
+ PlayerRoutes.createApplicationRequestBody(clientType, videoId, "RD$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 sendWebRequest(videoId: String): JSONObject? {
+ Objects.requireNonNull(videoId)
+
+ val startTime = System.currentTimeMillis()
+ val clientType = WebClient.ClientType.MWEB
+ val clientTypeName = clientType.name
+ Logger.printDebug { "Fetching playability request for: $videoId using client: $clientTypeName" }
+
+ try {
+ val connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(
+ PlayerRoutes.GET_CATEGORY,
+ clientType
+ )
+ val requestBody =
+ PlayerRoutes.createWebInnertubeBody(clientType, 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({ "sendWebRequest failed" }, ex)
+ } finally {
+ Logger.printDebug { "video: " + videoId + " took: " + (System.currentTimeMillis() - startTime) + "ms" }
+ }
+
+ return null
+ }
+
+ private fun parseApplicationResponse(playlistJson: JSONObject): Boolean {
+ try {
+ val playerParams: String? = (playlistJson
+ .getJSONObject("contents")
+ .getJSONObject("singleColumnWatchNextResults")
+ .getJSONObject("playlist")
+ .getJSONObject("playlist")
+ .getJSONArray("contents")[0] as JSONObject)
+ .getJSONObject("playlistPanelVideoRenderer")
+ .getJSONObject("navigationEndpoint")
+ .getJSONObject("watchEndpoint")
+ .getString("playerParams")
+
+ return VideoInformation.isMixPlaylistsOpenedByUser(playerParams!!)
+ } catch (e: JSONException) {
+ Logger.printDebug { "Fetch failed while processing Application response data for response: $playlistJson" }
+ }
+
+ return false
+ }
+
+ private fun parseWebResponse(microFormatJson: JSONObject): Boolean {
+ try {
+ return microFormatJson
+ .getJSONObject("playerMicroformatRenderer")
+ .getJSONObject("category")
+ .getString("status")
+ .equals("Music")
+ } catch (e: JSONException) {
+ Logger.printDebug { "Fetch failed while processing Web response data for response: $microFormatJson" }
+ }
+
+ return false
+ }
+
+ private fun fetch(videoId: String, checkCategory: Boolean): Boolean {
+ if (checkCategory) {
+ val microFormatJson = sendWebRequest(videoId)
+ if (microFormatJson != null) {
+ return parseWebResponse(microFormatJson)
+ }
+ } else {
+ val playlistJson = sendApplicationRequest(videoId)
+ if (playlistJson != null) {
+ return parseApplicationResponse(playlistJson)
+ }
+ }
+
+ return false
+ }
+ }
+}
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/requests/PlaylistRequest.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/requests/PlaylistRequest.java
deleted file mode 100644
index b7c69cd0a..000000000
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/requests/PlaylistRequest.java
+++ /dev/null
@@ -1,202 +0,0 @@
-package app.revanced.extension.youtube.patches.video.requests;
-
-import static app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes.GET_PLAYLIST_PAGE;
-
-import android.annotation.SuppressLint;
-
-import androidx.annotation.GuardedBy;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import java.io.IOException;
-import java.net.HttpURLConnection;
-import java.net.SocketTimeoutException;
-import java.nio.charset.StandardCharsets;
-import java.util.HashMap;
-import java.util.Locale;
-import java.util.Map;
-import java.util.Objects;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.Future;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-
-import app.revanced.extension.shared.patches.client.AppClient.ClientType;
-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.shared.VideoInformation;
-
-public class PlaylistRequest {
-
- /**
- * How long to keep fetches until they are expired.
- */
- private static final long CACHE_RETENTION_TIME_MILLISECONDS = 60 * 1000; // 1 Minute
-
- private static final long MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000; // 20 seconds
-
- @GuardedBy("itself")
- private static final Map cache = new HashMap<>();
-
- @SuppressLint("ObsoleteSdkInt")
- public static void fetchRequestIfNeeded(@Nullable String videoId) {
- Objects.requireNonNull(videoId);
- synchronized (cache) {
- final long now = System.currentTimeMillis();
-
- cache.values().removeIf(request -> {
- final boolean expired = request.isExpired(now);
- if (expired) Logger.printDebug(() -> "Removing expired stream: " + request.videoId);
- return expired;
- });
-
- if (!cache.containsKey(videoId)) {
- cache.put(videoId, new PlaylistRequest(videoId));
- }
- }
- }
-
- @Nullable
- public static PlaylistRequest getRequestForVideoId(@Nullable String videoId) {
- synchronized (cache) {
- return cache.get(videoId);
- }
- }
-
- private static void handleConnectionError(String toastMessage, @Nullable Exception ex) {
- Logger.printInfo(() -> toastMessage, ex);
- }
-
- @Nullable
- private static JSONObject send(ClientType clientType, String videoId) {
- Objects.requireNonNull(clientType);
- Objects.requireNonNull(videoId);
-
- final long startTime = System.currentTimeMillis();
- String clientTypeName = clientType.name();
- Logger.printDebug(() -> "Fetching playlist request for: " + videoId + " using client: " + clientTypeName);
-
- try {
- HttpURLConnection connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(GET_PLAYLIST_PAGE, clientType);
-
- String innerTubeBody = String.format(
- Locale.ENGLISH,
- PlayerRoutes.createInnertubeBody(clientType, true),
- videoId,
- "RD" + videoId
- );
- byte[] requestBody = innerTubeBody.getBytes(StandardCharsets.UTF_8);
- connection.setFixedLengthStreamingMode(requestBody.length);
- connection.getOutputStream().write(requestBody);
-
- final int responseCode = connection.getResponseCode();
- if (responseCode == 200) return Requester.parseJSONObject(connection);
-
- handleConnectionError(clientTypeName + " not available with response code: "
- + responseCode + " message: " + connection.getResponseMessage(),
- null);
- } catch (SocketTimeoutException ex) {
- handleConnectionError("Connection timeout", ex);
- } catch (IOException ex) {
- handleConnectionError("Network error", ex);
- } catch (Exception ex) {
- Logger.printException(() -> "send failed", ex);
- } finally {
- Logger.printDebug(() -> "video: " + videoId + " took: " + (System.currentTimeMillis() - startTime) + "ms");
- }
-
- return null;
- }
-
- private static Boolean fetch(@NonNull String videoId) {
- final ClientType clientType = ClientType.ANDROID_VR;
- final JSONObject playlistJson = send(clientType, videoId);
- if (playlistJson != null) {
- try {
- final JSONObject singleColumnWatchNextResultsJsonObject = playlistJson
- .getJSONObject("contents")
- .getJSONObject("singleColumnWatchNextResults");
-
- if (!singleColumnWatchNextResultsJsonObject.has("playlist")) {
- return false;
- }
-
- final JSONObject playlistJsonObject = singleColumnWatchNextResultsJsonObject
- .getJSONObject("playlist")
- .getJSONObject("playlist");
-
- final Object currentStreamObject = playlistJsonObject
- .getJSONArray("contents")
- .get(0);
-
- if (!(currentStreamObject instanceof JSONObject currentStreamJsonObject)) {
- return false;
- }
-
- final JSONObject watchEndpointJsonObject = currentStreamJsonObject
- .getJSONObject("playlistPanelVideoRenderer")
- .getJSONObject("navigationEndpoint")
- .getJSONObject("watchEndpoint");
-
- Logger.printDebug(() -> "watchEndpoint: " + watchEndpointJsonObject);
-
- return watchEndpointJsonObject.has("playerParams") &&
- VideoInformation.isMixPlaylistsOpenedByUser(watchEndpointJsonObject.getString("playerParams"));
- } catch (JSONException e) {
- Logger.printDebug(() -> "Fetch failed while processing response data for response: " + playlistJson);
- }
- }
-
- return false;
- }
-
- /**
- * Time this instance and the fetch future was created.
- */
- private final long timeFetched;
- private final String videoId;
- private final Future future;
-
- private PlaylistRequest(String videoId) {
- this.timeFetched = System.currentTimeMillis();
- this.videoId = videoId;
- this.future = Utils.submitOnBackgroundThread(() -> fetch(videoId));
- }
-
- public boolean isExpired(long now) {
- final long timeSinceCreation = now - timeFetched;
- if (timeSinceCreation > CACHE_RETENTION_TIME_MILLISECONDS) {
- return true;
- }
-
- // Only expired if the fetch failed (API null response).
- return (fetchCompleted() && getStream() == null);
- }
-
- /**
- * @return if the fetch call has completed.
- */
- public boolean fetchCompleted() {
- return future.isDone();
- }
-
- public Boolean getStream() {
- try {
- return future.get(MAX_MILLISECONDS_TO_WAIT_FOR_FETCH, TimeUnit.MILLISECONDS);
- } catch (TimeoutException ex) {
- Logger.printInfo(() -> "getStream timed out", ex);
- } catch (InterruptedException ex) {
- Logger.printException(() -> "getStream interrupted", ex);
- Thread.currentThread().interrupt(); // Restore interrupt status flag.
- } catch (ExecutionException ex) {
- Logger.printException(() -> "getStream failure", ex);
- }
-
- return null;
- }
-}
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/ReturnYouTubeDislike.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/ReturnYouTubeDislike.java
index dd478f4f0..c2d727885 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/ReturnYouTubeDislike.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/ReturnYouTubeDislike.java
@@ -118,7 +118,6 @@ public class ReturnYouTubeDislike {
*/
public static final int leftSeparatorShapePaddingPixels;
private static final ShapeDrawable leftSeparatorShape;
- public static final Locale locale;
static {
final Resources resources = Utils.getResources();
@@ -135,7 +134,6 @@ public class ReturnYouTubeDislike {
leftSeparatorShape = new ShapeDrawable(new RectShape());
leftSeparatorShape.setBounds(leftSeparatorBounds);
- locale = resources.getConfiguration().getLocales().get(0);
ReturnYouTubeDislikeApi.toastOnConnectionError = Settings.RYD_TOAST_ON_CONNECTION_ERROR.get();
}
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/Settings.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/Settings.java
index 9d593c8f5..043af0fee 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/Settings.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/Settings.java
@@ -38,6 +38,7 @@ import app.revanced.extension.youtube.patches.general.LayoutSwitchPatch.FormFact
import app.revanced.extension.youtube.patches.general.MiniplayerPatch;
import app.revanced.extension.youtube.patches.general.YouTubeMusicActionsPatch;
import app.revanced.extension.youtube.patches.misc.WatchHistoryPatch.WatchHistoryType;
+import app.revanced.extension.youtube.patches.player.ExitFullscreenPatch.FullscreenMode;
import app.revanced.extension.youtube.patches.shorts.AnimationFeedbackPatch.AnimationType;
import app.revanced.extension.youtube.patches.shorts.ShortsRepeatStatePatch.ShortsLoopBehavior;
import app.revanced.extension.youtube.patches.utils.PatchStatus;
@@ -147,7 +148,7 @@ public class Settings extends BaseSettings {
new ChangeStartPagePatch.ChangeStartPageTypeAvailability());
public static final BooleanSetting DISABLE_AUTO_AUDIO_TRACKS = new BooleanSetting("revanced_disable_auto_audio_tracks", FALSE);
public static final BooleanSetting DISABLE_SPLASH_ANIMATION = new BooleanSetting("revanced_disable_splash_animation", FALSE, true);
- public static final BooleanSetting DISABLE_TRANSLUCENT_STATUS_BAR = new BooleanSetting("revanced_disable_translucent_status_bar", FALSE, true);
+ public static final BooleanSetting DISABLE_TRANSLUCENT_STATUS_BAR = new BooleanSetting("revanced_disable_translucent_status_bar", TRUE, true);
public static final BooleanSetting ENABLE_GRADIENT_LOADING_SCREEN = new BooleanSetting("revanced_enable_gradient_loading_screen", FALSE, true);
public static final BooleanSetting HIDE_FLOATING_MICROPHONE = new BooleanSetting("revanced_hide_floating_microphone", TRUE, true);
public static final BooleanSetting HIDE_GRAY_SEPARATOR = new BooleanSetting("revanced_hide_gray_separator", TRUE);
@@ -190,8 +191,7 @@ public class Settings extends BaseSettings {
public static final BooleanSetting HIDE_NAVIGATION_SUBSCRIPTIONS_BUTTON = new BooleanSetting("revanced_hide_navigation_subscriptions_button", FALSE, true);
public static final BooleanSetting HIDE_NAVIGATION_LABEL = new BooleanSetting("revanced_hide_navigation_label", FALSE, true);
public static final BooleanSetting SWITCH_CREATE_WITH_NOTIFICATIONS_BUTTON = new BooleanSetting("revanced_switch_create_with_notifications_button", TRUE, true, "revanced_switch_create_with_notifications_button_user_dialog_message");
- public static final BooleanSetting DISABLE_TRANSLUCENT_NAVIGATION_BAR_LIGHT = new BooleanSetting("revanced_disable_translucent_navigation_bar_light", FALSE, true);
- public static final BooleanSetting DISABLE_TRANSLUCENT_NAVIGATION_BAR_DARK = new BooleanSetting("revanced_disable_translucent_navigation_bar_dark", FALSE, true);
+ public static final BooleanSetting ENABLE_TRANSLUCENT_NAVIGATION_BAR = new BooleanSetting("revanced_enable_translucent_navigation_bar", FALSE, true);
public static final BooleanSetting HIDE_NAVIGATION_BAR = new BooleanSetting("revanced_hide_navigation_bar", FALSE, true);
// PreferenceScreen: General - Override buttons
@@ -336,6 +336,8 @@ public class Settings extends BaseSettings {
// PreferenceScreen: Player - Fullscreen
public static final BooleanSetting DISABLE_ENGAGEMENT_PANEL = new BooleanSetting("revanced_disable_engagement_panel", FALSE, true);
+ public static final BooleanSetting ENTER_FULLSCREEN = new BooleanSetting("revanced_enter_fullscreen", FALSE);
+ public static final EnumSetting EXIT_FULLSCREEN = new EnumSetting<>("revanced_exit_fullscreen", FullscreenMode.DISABLED);
public static final BooleanSetting SHOW_VIDEO_TITLE_SECTION = new BooleanSetting("revanced_show_video_title_section", TRUE, true, parent(DISABLE_ENGAGEMENT_PANEL));
public static final BooleanSetting HIDE_AUTOPLAY_PREVIEW = new BooleanSetting("revanced_hide_autoplay_preview", FALSE, true);
public static final BooleanSetting HIDE_LIVE_CHAT_REPLAY_BUTTON = new BooleanSetting("revanced_hide_live_chat_replay_button", FALSE);
@@ -355,7 +357,6 @@ public class Settings extends BaseSettings {
public static final BooleanSetting DISABLE_LANDSCAPE_MODE = new BooleanSetting("revanced_disable_landscape_mode", FALSE, true);
public static final BooleanSetting ENABLE_COMPACT_CONTROLS_OVERLAY = new BooleanSetting("revanced_enable_compact_controls_overlay", FALSE, true);
- public static final BooleanSetting FORCE_FULLSCREEN = new BooleanSetting("revanced_force_fullscreen", FALSE, true);
public static final BooleanSetting KEEP_LANDSCAPE_MODE = new BooleanSetting("revanced_keep_landscape_mode", FALSE, true);
public static final LongSetting KEEP_LANDSCAPE_MODE_TIMEOUT = new LongSetting("revanced_keep_landscape_mode_timeout", 3000L, true);
@@ -515,8 +516,10 @@ public class Settings extends BaseSettings {
*/
@Deprecated // Patch is obsolete and no longer works with 19.09+
public static final BooleanSetting DISABLE_HDR_AUTO_BRIGHTNESS = new BooleanSetting("revanced_disable_hdr_auto_brightness", TRUE, true, parent(ENABLE_SWIPE_BRIGHTNESS));
- public static final BooleanSetting DISABLE_SWIPE_TO_SWITCH_VIDEO = new BooleanSetting("revanced_disable_swipe_to_switch_video", FALSE, true);
- public static final BooleanSetting DISABLE_WATCH_PANEL_GESTURES = new BooleanSetting("revanced_disable_watch_panel_gestures", FALSE, true);
+ public static final BooleanSetting DISABLE_SWIPE_TO_SWITCH_VIDEO = new BooleanSetting("revanced_disable_swipe_to_switch_video", TRUE, true);
+ public static final BooleanSetting DISABLE_SWIPE_TO_ENTER_FULLSCREEN_MODE_BELOW_THE_PLAYER = new BooleanSetting("revanced_disable_swipe_to_enter_fullscreen_mode_below_the_player", TRUE, true);
+ public static final BooleanSetting DISABLE_SWIPE_TO_ENTER_FULLSCREEN_MODE_IN_THE_PLAYER = new BooleanSetting("revanced_disable_swipe_to_enter_fullscreen_mode_in_the_player", FALSE, true);
+ public static final BooleanSetting DISABLE_SWIPE_TO_EXIT_FULLSCREEN_MODE = new BooleanSetting("revanced_disable_swipe_to_exit_fullscreen_mode", FALSE, true);
public static final BooleanSetting SWIPE_BRIGHTNESS_AUTO = new BooleanSetting("revanced_swipe_brightness_auto", TRUE, false, false);
public static final FloatSetting SWIPE_BRIGHTNESS_VALUE = new FloatSetting("revanced_swipe_brightness_value", -1.0f, false, false);
@@ -526,7 +529,6 @@ public class Settings extends BaseSettings {
public static final IntegerSetting DEFAULT_VIDEO_QUALITY_MOBILE = new IntegerSetting("revanced_default_video_quality_mobile", -2);
public static final IntegerSetting DEFAULT_VIDEO_QUALITY_WIFI = new IntegerSetting("revanced_default_video_quality_wifi", -2);
public static final BooleanSetting DISABLE_HDR_VIDEO = new BooleanSetting("revanced_disable_hdr_video", FALSE, true);
- public static final BooleanSetting DISABLE_DEFAULT_PLAYBACK_SPEED_LIVE = new BooleanSetting("revanced_disable_default_playback_speed_live", TRUE);
public static final BooleanSetting ENABLE_CUSTOM_PLAYBACK_SPEED = new BooleanSetting("revanced_enable_custom_playback_speed", FALSE, true);
public static final BooleanSetting CUSTOM_PLAYBACK_SPEED_MENU_TYPE = new BooleanSetting("revanced_custom_playback_speed_menu_type", FALSE, parent(ENABLE_CUSTOM_PLAYBACK_SPEED));
public static final StringSetting CUSTOM_PLAYBACK_SPEEDS = new StringSetting("revanced_custom_playback_speeds", "0.25\n0.5\n0.75\n1.0\n1.25\n1.5\n1.75\n2.0\n2.25\n2.5", true, parent(ENABLE_CUSTOM_PLAYBACK_SPEED));
@@ -537,6 +539,7 @@ public class Settings extends BaseSettings {
public static final BooleanSetting RESTORE_OLD_VIDEO_QUALITY_MENU = new BooleanSetting("revanced_restore_old_video_quality_menu", TRUE, true);
// Experimental Flags
public static final BooleanSetting DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC = new BooleanSetting("revanced_disable_default_playback_speed_music", FALSE, true);
+ public static final BooleanSetting DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC_TYPE = new BooleanSetting("revanced_disable_default_playback_speed_music_type", FALSE, true, parent(DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC));
public static final BooleanSetting ENABLE_DEFAULT_PLAYBACK_SPEED_SHORTS = new BooleanSetting("revanced_enable_default_playback_speed_shorts", FALSE);
public static final BooleanSetting SKIP_PRELOADED_BUFFER = new BooleanSetting("revanced_skip_preloaded_buffer", FALSE, true, "revanced_skip_preloaded_buffer_user_dialog_message");
public static final BooleanSetting SKIP_PRELOADED_BUFFER_TOAST = new BooleanSetting("revanced_skip_preloaded_buffer_toast", TRUE);
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/ReVancedPreferenceFragment.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/ReVancedPreferenceFragment.java
index cf46d1a79..d5abb90e7 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/ReVancedPreferenceFragment.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/ReVancedPreferenceFragment.java
@@ -1,5 +1,6 @@
package app.revanced.extension.youtube.settings.preference;
+import static com.google.android.apps.youtube.app.settings.videoquality.VideoQualitySettingsActivity.setToolbarLayoutParams;
import static app.revanced.extension.shared.settings.preference.AbstractPreferenceFragment.showRestartDialog;
import static app.revanced.extension.shared.settings.preference.AbstractPreferenceFragment.updateListPreferenceSummary;
import static app.revanced.extension.shared.utils.ResourceUtils.getXmlIdentifier;
@@ -18,6 +19,7 @@ import android.app.Dialog;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
+import android.graphics.Insets;
import android.net.Uri;
import android.os.Bundle;
import android.preference.EditTextPreference;
@@ -31,6 +33,7 @@ import android.preference.PreferenceScreen;
import android.preference.SwitchPreference;
import android.util.TypedValue;
import android.view.ViewGroup;
+import android.view.WindowInsets;
import android.widget.TextView;
import android.widget.Toolbar;
@@ -202,6 +205,15 @@ public class ReVancedPreferenceFragment extends PreferenceFragment {
.findViewById(android.R.id.content)
.getParent();
+ // Fix required for Android 15
+ if (isSDKAbove(35)) {
+ rootView.setOnApplyWindowInsetsListener((v, insets) -> {
+ Insets statusInsets = insets.getInsets(WindowInsets.Type.statusBars());
+ v.setPadding(0, statusInsets.top, 0, 0);
+ return insets;
+ });
+ }
+
Toolbar toolbar = new Toolbar(preferenceScreen.getContext());
toolbar.setTitle(preferenceScreen.getTitle());
@@ -219,6 +231,8 @@ public class ReVancedPreferenceFragment extends PreferenceFragment {
toolbarTextView.setTextColor(ThemeUtils.getForegroundColor());
}
+ setToolbarLayoutParams(toolbar);
+
rootView.addView(toolbar, 0);
return false;
}
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/ReVancedSettingsPreference.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/ReVancedSettingsPreference.java
index d1dfbcdb4..a3987e7b9 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/ReVancedSettingsPreference.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/ReVancedSettingsPreference.java
@@ -46,10 +46,8 @@ public class ReVancedSettingsPreference extends ReVancedPreferenceFragment {
enableDisablePreferences();
AmbientModePreferenceLinks();
- ChangeHeaderPreferenceLinks();
ExternalDownloaderPreferenceLinks();
FullScreenPanelPreferenceLinks();
- LayoutOverrideLinks();
MiniPlayerPreferenceLinks();
NavigationPreferenceLinks();
RYDPreferenceLinks();
@@ -71,16 +69,6 @@ public class ReVancedSettingsPreference extends ReVancedPreferenceFragment {
);
}
- /**
- * Enable/Disable Preference related to Change header
- */
- private static void ChangeHeaderPreferenceLinks() {
- enableDisablePreferences(
- PatchStatus.MinimalHeader(),
- Settings.CHANGE_YOUTUBE_HEADER
- );
- }
-
/**
* Enable/Disable Preference for External downloader settings
*/
@@ -93,16 +81,6 @@ public class ReVancedSettingsPreference extends ReVancedPreferenceFragment {
);
}
- /**
- * Enable/Disable Layout Override Preference
- */
- private static void LayoutOverrideLinks() {
- enableDisablePreferences(
- ExtendedUtils.isTablet(),
- Settings.FORCE_FULLSCREEN
- );
- }
-
/**
* Enable/Disable Preferences not working in tablet layout
*/
@@ -139,17 +117,6 @@ public class ReVancedSettingsPreference extends ReVancedPreferenceFragment {
Settings.HIDE_QUICK_ACTIONS_SAVE_TO_PLAYLIST_BUTTON,
Settings.HIDE_QUICK_ACTIONS_SHARE_BUTTON
);
-
- enableDisablePreferences(
- Settings.DISABLE_LANDSCAPE_MODE.get(),
- Settings.FORCE_FULLSCREEN
- );
-
- enableDisablePreferences(
- Settings.FORCE_FULLSCREEN.get(),
- Settings.DISABLE_LANDSCAPE_MODE
- );
-
}
/**
@@ -204,9 +171,8 @@ public class ReVancedSettingsPreference extends ReVancedPreferenceFragment {
Settings.REPLACE_TOOLBAR_CREATE_BUTTON_TYPE
);
enableDisablePreferences(
- !isSDKAbove(33),
- Settings.DISABLE_TRANSLUCENT_NAVIGATION_BAR_LIGHT,
- Settings.DISABLE_TRANSLUCENT_NAVIGATION_BAR_DARK
+ !isSDKAbove(31),
+ Settings.ENABLE_TRANSLUCENT_NAVIGATION_BAR
);
}
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/SpoofStreamingDataDefaultClientListPreference.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/SpoofStreamingDataDefaultClientListPreference.java
deleted file mode 100644
index b3fabe111..000000000
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/SpoofStreamingDataDefaultClientListPreference.java
+++ /dev/null
@@ -1,87 +0,0 @@
-package app.revanced.extension.youtube.settings.preference;
-
-import static app.revanced.extension.shared.utils.ResourceUtils.getStringArray;
-
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.preference.ListPreference;
-import android.preference.PreferenceManager;
-import android.util.AttributeSet;
-
-import app.revanced.extension.shared.patches.client.AppClient.ClientType;
-import app.revanced.extension.shared.settings.EnumSetting;
-import app.revanced.extension.shared.settings.Setting;
-import app.revanced.extension.shared.utils.Utils;
-import app.revanced.extension.youtube.settings.Settings;
-
-@SuppressWarnings({"unused", "deprecation"})
-public class SpoofStreamingDataDefaultClientListPreference extends ListPreference {
-
- private final SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, str) -> {
- // Because this listener may run before the ReVanced settings fragment updates Settings,
- // this could show the prior config and not the current.
- //
- // Push this call to the end of the main run queue,
- // so all other listeners are done and Settings is up to date.
- Utils.runOnMainThread(this::updateUI);
- };
-
- public SpoofStreamingDataDefaultClientListPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
- super(context, attrs, defStyleAttr, defStyleRes);
- }
-
- public SpoofStreamingDataDefaultClientListPreference(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- }
-
- public SpoofStreamingDataDefaultClientListPreference(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- public SpoofStreamingDataDefaultClientListPreference(Context context) {
- super(context);
- }
-
- private void addChangeListener() {
- Setting.preferences.preferences.registerOnSharedPreferenceChangeListener(listener);
- }
-
- private void removeChangeListener() {
- Setting.preferences.preferences.unregisterOnSharedPreferenceChangeListener(listener);
- }
-
- @Override
- protected void onAttachedToHierarchy(PreferenceManager preferenceManager) {
- super.onAttachedToHierarchy(preferenceManager);
- updateUI();
- addChangeListener();
- }
-
- @Override
- protected void onPrepareForRemoval() {
- super.onPrepareForRemoval();
- removeChangeListener();
- }
-
- private void updateUI() {
- final boolean spoofStreamingDataAndroidOnly = Settings.SPOOF_STREAMING_DATA_ANDROID_ONLY.get();
- final String entryKey = spoofStreamingDataAndroidOnly
- ? "revanced_spoof_streaming_data_type_android_entries"
- : "revanced_spoof_streaming_data_type_android_ios_entries";
- final String entryValueKey = spoofStreamingDataAndroidOnly
- ? "revanced_spoof_streaming_data_type_android_entry_values"
- : "revanced_spoof_streaming_data_type_android_ios_entry_values";
- final String[] mEntries = getStringArray(entryKey);
- final String[] mEntryValues = getStringArray(entryValueKey);
- setEntries(mEntries);
- setEntryValues(mEntryValues);
-
- final EnumSetting clientType = Settings.SPOOF_STREAMING_DATA_TYPE;
- final boolean isAndroid = clientType.get().name().startsWith("ANDROID");
- if (spoofStreamingDataAndroidOnly && !isAndroid) {
- clientType.resetToDefault();
- }
-
- setEnabled(Settings.SPOOF_STREAMING_DATA.get());
- }
-}
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/SpoofStreamingDataSideEffectsPreference.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/SpoofStreamingDataSideEffectsPreference.java
index f8e062270..99958d018 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/SpoofStreamingDataSideEffectsPreference.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/SpoofStreamingDataSideEffectsPreference.java
@@ -63,13 +63,7 @@ public class SpoofStreamingDataSideEffectsPreference extends Preference {
private void updateUI() {
final String clientName = Settings.SPOOF_STREAMING_DATA_TYPE.get().name().toLowerCase();
- String summaryTextKey = "revanced_spoof_streaming_data_side_effects_";
-
- if (Settings.SPOOF_STREAMING_DATA_ANDROID_ONLY.get()) {
- summaryTextKey += "android";
- } else {
- summaryTextKey += clientName;
- }
+ final String summaryTextKey = "revanced_spoof_streaming_data_side_effects_" + clientName;
setSummary(str(summaryTextKey));
setEnabled(Settings.SPOOF_STREAMING_DATA.get());
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/BottomSheetState.kt b/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/BottomSheetState.kt
index 2d8b513a3..83e4dd514 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/BottomSheetState.kt
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/BottomSheetState.kt
@@ -31,7 +31,7 @@ enum class BottomSheetState {
onChange(currentBottomSheetState)
}
- @Volatile // value is read/write from different threads
+ @Volatile // Read/write from different threads.
private var currentBottomSheetState = CLOSED
/**
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/LockModeState.kt b/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/LockModeState.kt
index 9a330e687..2b7063664 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/LockModeState.kt
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/LockModeState.kt
@@ -40,7 +40,7 @@ enum class LockModeState {
onChange(value)
}
- @Volatile // value is read/write from different threads
+ @Volatile // Read/write from different threads.
private var currentLockModeState = LOCK_MODE_STATE_ENUM_UNKNOWN
/**
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/NavigationBar.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/NavigationBar.java
index 0f3c07105..3c65e1d0c 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/NavigationBar.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/NavigationBar.java
@@ -25,20 +25,21 @@ public final class NavigationBar {
* How long to wait for the set nav button latch to be released. Maximum wait time must
* be as small as possible while still allowing enough time for the nav bar to update.
*
- * YT calls it's back button handlers out of order,
- * and litho starts filtering before the navigation bar is updated.
+ * YT calls it's back button handlers out of order, and litho starts filtering before the
+ * navigation bar is updated. Fixing this situation and not needlessly waiting requires
+ * somehow detecting if a back button key/gesture will not change the active tab.
*
- * Fixing this situation and not needlessly waiting requires somehow
- * detecting if a back button key-press will cause a tab change.
+ * On average the time between pressing the back button and the first litho event is
+ * about 10-20ms. Waiting up to 75-150ms should be enough time to handle normal use cases
+ * and not be noticeable, since YT typically takes 100-200ms (or more) to update the view.
*
- * Typically after pressing the back button, the time between the first litho event and
- * when the nav button is updated is about 10-20ms. Using 50-100ms here should be enough time
- * and not noticeable, since YT typically takes 100-200ms (or more) to update the view anyways.
+ * This delay is only noticeable when the device back button/gesture will not
+ * change the current navigation tab, such as backing out of the watch history.
*
* This issue can also be avoided on a patch by patch basis, by avoiding calls to
* {@link NavigationButton#getSelectedNavigationButton()} unless absolutely necessary.
*/
- private static final long LATCH_AWAIT_TIMEOUT_MILLISECONDS = 75;
+ private static final long LATCH_AWAIT_TIMEOUT_MILLISECONDS = 120;
/**
* Used as a workaround to fix the issue of YT calling back button handlers out of order.
@@ -84,7 +85,8 @@ public final class NavigationBar {
// The latch is released from the main thread, and waiting from the main thread will always timeout.
// This situation has only been observed when navigating out of a submenu and not changing tabs.
// and for that use case the nav bar does not change so it's safe to return here.
- Logger.printDebug(() -> "Cannot block main thread waiting for nav button. Using last known navbar button status.");
+ Logger.printDebug(() -> "Cannot block main thread waiting for nav button. " +
+ "Using last known navbar button status.");
return;
}
@@ -102,7 +104,9 @@ public final class NavigationBar {
Logger.printDebug(() -> "Latch wait timed out");
} catch (InterruptedException ex) {
- Logger.printException(() -> "Latch wait interrupted failure", ex); // Will never happen.
+ // Calling YouTube thread was interrupted.
+ Logger.printException(() -> "Latch wait interrupted", ex);
+ Thread.currentThread().interrupt(); // Restore interrupt status flag.
}
}
@@ -256,8 +260,8 @@ public final class NavigationBar {
*
* All code calling this method should handle a null return value.
*
- * Due to issues with how YT processes physical back button events,
- * this patch uses workarounds that can cause this method to take up to 75ms
+ * Due to issues with how YT processes physical back button/gesture events,
+ * this patch uses workarounds that can cause this method to take up to 120ms
* if the device back button was recently pressed.
*
* @return The active navigation tab.
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/PlayerControlsVisibility.kt b/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/PlayerControlsVisibility.kt
index e9d5468d4..c7cf1bbd7 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/PlayerControlsVisibility.kt
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/PlayerControlsVisibility.kt
@@ -14,7 +14,7 @@ enum class PlayerControlsVisibility {
companion object {
- private val nameToPlayerControlsVisibility = values().associateBy { it.name }
+ private val nameToPlayerControlsVisibility = entries.associateBy { it.name }
@JvmStatic
fun setFromString(enumName: String) {
@@ -38,6 +38,7 @@ enum class PlayerControlsVisibility {
currentPlayerControlsVisibility = value
}
+ @Volatile // Read/write from different threads.
private var currentPlayerControlsVisibility: PlayerControlsVisibility? = null
}
}
\ No newline at end of file
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/PlayerType.kt b/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/PlayerType.kt
index 9bfaffe58..201d32085 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/PlayerType.kt
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/PlayerType.kt
@@ -71,7 +71,7 @@ enum class PlayerType {
onChange(value)
}
- @Volatile // value is read/write from different threads
+ @Volatile // Read/write from different threads.
private var currentPlayerType = NONE
/**
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/ShortsPlayerState.kt b/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/ShortsPlayerState.kt
index b0aed2e79..e3e56c58e 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/ShortsPlayerState.kt
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/ShortsPlayerState.kt
@@ -31,7 +31,7 @@ enum class ShortsPlayerState {
onChange(value)
}
- @Volatile // value is read/write from different threads
+ @Volatile // Read/write from different threads.
private var currentShortsPlayerState = CLOSED
/**
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/VideoState.kt b/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/VideoState.kt
index 4e1888a7c..b26759ce9 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/VideoState.kt
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/VideoState.kt
@@ -39,6 +39,7 @@ enum class VideoState {
currentVideoState = value
}
+ @Volatile // Read/write from different threads.
private var currentVideoState: VideoState? = null
}
}
\ No newline at end of file
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/requests/SBRequester.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/requests/SBRequester.java
index f2e31a014..b0679a902 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/requests/SBRequester.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/requests/SBRequester.java
@@ -126,8 +126,12 @@ public class SBRequester {
HttpURLConnection connection = getConnectionFromRoute(SBRoutes.SUBMIT_SEGMENTS, privateUserId, videoId, category, start, end, duration);
final int responseCode = connection.getResponseCode();
- String userMessage = switch (responseCode) {
- case HTTP_STATUS_CODE_SUCCESS -> str("revanced_sb_submit_succeeded");
+ if (responseCode == HTTP_STATUS_CODE_SUCCESS) {
+ Utils.showToastLong(str("revanced_sb_submit_succeeded"));
+ return;
+ }
+
+ String userErrorMessage = switch (responseCode) {
case 409 -> str("revanced_sb_submit_failed_duplicate");
case 403 -> str("revanced_sb_submit_failed_forbidden",
Requester.parseErrorStringAndDisconnect(connection));
@@ -137,9 +141,10 @@ public class SBRequester {
default -> str("revanced_sb_submit_failed_unknown_error",
responseCode, connection.getResponseMessage());
};
+
// Message might be about the users account or an error too large to show in a toast.
// Use a dialog instead.
- SponsorBlockUtils.showErrorDialog(userMessage);
+ SponsorBlockUtils.showErrorDialog(userErrorMessage);
} catch (SocketTimeoutException ex) {
Logger.printDebug(() -> "Timeout", ex);
Utils.showToastLong(str("revanced_sb_submit_failed_timeout"));
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/controller/gesture/core/BaseGestureController.kt b/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/controller/gesture/core/BaseGestureController.kt
index ac995bfd7..314fb3d76 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/controller/gesture/core/BaseGestureController.kt
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/controller/gesture/core/BaseGestureController.kt
@@ -87,8 +87,10 @@ abstract class BaseGestureController(
distanceX: Float,
distanceY: Float,
): Boolean {
+ if (from == null) return false
+
// submit to swipe detector
- submitForSwipe(from!!, to, distanceX, distanceY)
+ submitForSwipe(from, to, distanceX, distanceY)
// call swipe callback if in a swipe
return if (currentSwipe != SwipeDetector.SwipeDirection.NONE) {
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/utils/ExtendedUtils.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/utils/ExtendedUtils.java
index 77f328b52..3af96e1c8 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/utils/ExtendedUtils.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/utils/ExtendedUtils.java
@@ -16,6 +16,7 @@ public class ExtendedUtils extends PackageUtils {
public static final boolean IS_19_20_OR_GREATER = getAppVersionName().compareTo("19.20.00") >= 0;
public static final boolean IS_19_21_OR_GREATER = getAppVersionName().compareTo("19.21.00") >= 0;
public static final boolean IS_19_26_OR_GREATER = getAppVersionName().compareTo("19.26.00") >= 0;
+ public static final boolean IS_19_28_OR_GREATER = getAppVersionName().compareTo("19.28.00") >= 0;
public static final boolean IS_19_29_OR_GREATER = getAppVersionName().compareTo("19.29.00") >= 0;
public static final boolean IS_19_34_OR_GREATER = getAppVersionName().compareTo("19.34.00") >= 0;
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/utils/VideoUtils.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/utils/VideoUtils.java
index 2ee1127b7..0a5eb34e6 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/utils/VideoUtils.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/utils/VideoUtils.java
@@ -242,6 +242,20 @@ public class VideoUtils extends IntentUtils {
return !isExternalDownloaderLaunched.get() && original;
}
+ /**
+ * Rest of the implementation added by patch.
+ */
+ public static void enterFullscreenMode() {
+ Logger.printDebug(() -> "Enter fullscreen mode");
+ }
+
+ /**
+ * Rest of the implementation added by patch.
+ */
+ public static void exitFullscreenMode() {
+ Logger.printDebug(() -> "Exit fullscreen mode");
+ }
+
/**
* Rest of the implementation added by patch.
*/
diff --git a/extensions/shared/src/main/java/com/google/android/apps/youtube/app/settings/videoquality/VideoQualitySettingsActivity.java b/extensions/shared/src/main/java/com/google/android/apps/youtube/app/settings/videoquality/VideoQualitySettingsActivity.java
index 1d1468478..14d19be64 100644
--- a/extensions/shared/src/main/java/com/google/android/apps/youtube/app/settings/videoquality/VideoQualitySettingsActivity.java
+++ b/extensions/shared/src/main/java/com/google/android/apps/youtube/app/settings/videoquality/VideoQualitySettingsActivity.java
@@ -4,7 +4,6 @@ import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.util.TypedValue;
-import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.SearchView;
@@ -88,17 +87,21 @@ public class VideoQualitySettingsActivity extends Activity {
fragment.filterPreferences(query);
}
+ private static ViewGroup.LayoutParams lp;
+
+ public static void setToolbarLayoutParams(Toolbar toolbar) {
+ if (lp != null) {
+ toolbar.setLayoutParams(lp);
+ }
+ }
+
private void setToolbar() {
- if (!(findViewById(ResourceUtils.getIdIdentifier("revanced_toolbar_parent")) instanceof ViewGroup toolBarParent))
- return;
+ ViewGroup toolBarParent = findViewById(ResourceUtils.getIdIdentifier("revanced_toolbar_parent"));
// Remove dummy toolbar.
- for (int i = 0; i < toolBarParent.getChildCount(); i++) {
- View view = toolBarParent.getChildAt(i);
- if (view != null) {
- toolBarParent.removeView(view);
- }
- }
+ ViewGroup dummyToolbar = toolBarParent.findViewById(ResourceUtils.getIdIdentifier("revanced_toolbar"));
+ lp = dummyToolbar.getLayoutParams();
+ toolBarParent.removeView(dummyToolbar);
Toolbar toolbar = new Toolbar(toolBarParent.getContext());
toolbar.setBackgroundColor(ThemeUtils.getToolbarBackgroundColor());
@@ -112,6 +115,7 @@ public class VideoQualitySettingsActivity extends Activity {
if (toolbarTextView != null) {
toolbarTextView.setTextColor(ThemeUtils.getForegroundColor());
}
+ setToolbarLayoutParams(toolbar);
toolBarParent.addView(toolbar, 0);
}
diff --git a/gradle.properties b/gradle.properties
index 9013491db..5de17c401 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -4,5 +4,5 @@ org.gradle.parallel = true
android.useAndroidX = true
kotlin.code.style = official
kotlin.jvm.target.validation.mode = IGNORE
-version = 5.1.3
+version = 5.2.1
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index fb602ee2a..e1b837a19 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,7 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionSha256Sum=31c55713e40233a8303827ceb42ca48a47267a0ad4bab9177123121e71524c26
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
+distributionSha256Sum=7a00d51fb93147819aab76024feece20b6b84e420694101f276be952e08bef03
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
diff --git a/gradlew b/gradlew
index f5feea6d6..f3b75f3b0 100755
--- a/gradlew
+++ b/gradlew
@@ -86,8 +86,7 @@ done
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
-APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
-' "$PWD" ) || exit
+APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
diff --git a/patches.json b/patches.json
index eac961c94..dccb9d4e1 100644
--- a/patches.json
+++ b/patches.json
@@ -43,26 +43,6 @@
},
"options": []
},
- {
- "name": "Amoled",
- "description": "Applies a pure black theme to some components.",
- "use": true,
- "dependencies": [
- "BytecodePatch",
- "Settings for YouTube Music"
- ],
- "compatiblePackages": {
- "com.google.android.apps.youtube.music": [
- "6.20.51",
- "6.29.59",
- "6.42.55",
- "6.51.53",
- "7.16.53",
- "7.25.53"
- ]
- },
- "options": []
- },
{
"name": "Bitrate default value",
"description": "Sets the audio quality to \u0027Always High\u0027 when you first install the app.",
@@ -186,7 +166,7 @@
},
{
"name": "Change share sheet",
- "description": "Add option to change from in-app share sheet to system share sheet.",
+ "description": "Adds an option to change the in-app share sheet to the system share sheet.",
"use": true,
"dependencies": [
"Settings for YouTube Music",
@@ -207,7 +187,7 @@
},
{
"name": "Change share sheet",
- "description": "Add option to change from in-app share sheet to system share sheet.",
+ "description": "Adds an option to change the in-app share sheet to the system share sheet.",
"use": true,
"dependencies": [
"Settings for YouTube",
@@ -364,7 +344,7 @@
{
"key": "changeSplashIcon",
"title": "Change splash icons",
- "description": "Apply the custom branding icon to the splash screen.",
+ "description": "Apply the custom branding icon to the splash screen. Supports from YouTube 18.29.38 to YouTube 19.16.39.",
"required": true,
"type": "kotlin.Boolean",
"default": true,
@@ -636,6 +616,53 @@
}
]
},
+ {
+ "name": "Dark theme",
+ "description": "Changes the app\u0027s dark theme to the values specified in patch options.",
+ "use": true,
+ "dependencies": [
+ "BytecodePatch"
+ ],
+ "compatiblePackages": {
+ "com.google.android.apps.youtube.music": [
+ "6.20.51",
+ "6.29.59",
+ "6.42.55",
+ "6.51.53",
+ "7.16.53",
+ "7.25.53"
+ ]
+ },
+ "options": [
+ {
+ "key": "darkThemeBackgroundColor",
+ "title": "Dark theme background color",
+ "description": "Can be a hex color (#AARRGGBB) or a color resource reference.",
+ "required": false,
+ "type": "kotlin.String",
+ "default": "@android:color/black",
+ "values": {
+ "Amoled Black": "@android:color/black",
+ "Catppuccin (Mocha)": "#FF181825",
+ "Dark Pink": "#FF290025",
+ "Dark Blue": "#FF001029",
+ "Dark Green": "#FF002905",
+ "Dark Yellow": "#FF282900",
+ "Dark Orange": "#FF291800",
+ "Dark Red": "#FF290000"
+ }
+ },
+ {
+ "key": "materialYou",
+ "title": "MaterialYou",
+ "description": "Applies the MaterialYou theme for Android 12+ devices.",
+ "required": true,
+ "type": "kotlin.Boolean",
+ "default": false,
+ "values": null
+ }
+ ]
+ },
{
"name": "Description components",
"description": "Adds options to hide and disable description components.",
@@ -819,6 +846,27 @@
},
"options": []
},
+ {
+ "name": "Disable music video in album",
+ "description": "Adds option to redirect music videos from albums for non-premium users.",
+ "use": false,
+ "dependencies": [
+ "Settings for YouTube Music",
+ "BytecodePatch",
+ "BytecodePatch"
+ ],
+ "compatiblePackages": {
+ "com.google.android.apps.youtube.music": [
+ "6.20.51",
+ "6.29.59",
+ "6.42.55",
+ "6.51.53",
+ "7.16.53",
+ "7.25.53"
+ ]
+ },
+ "options": []
+ },
{
"name": "Disable resuming Shorts on startup",
"description": "Adds an option to disable the Shorts player from resuming on app startup when Shorts were last being watched.",
@@ -1087,6 +1135,9 @@
"Settings for YouTube",
"BytecodePatch",
"BytecodePatch",
+ "BytecodePatch",
+ "BytecodePatch",
+ "BytecodePatch",
"ResourcePatch",
"ResourcePatch"
],
@@ -1493,7 +1544,8 @@
"BytecodePatch",
"ResourcePatch",
"BytecodePatch",
- "BytecodePatch"
+ "BytecodePatch",
+ "ResourcePatch"
],
"compatiblePackages": {
"com.google.android.youtube": [
@@ -1619,7 +1671,7 @@
},
{
"name": "Hide shortcuts",
- "description": "Remove, at compile time, the app shortcuts that appears when app icon is long pressed.",
+ "description": "Remove, at compile time, the app shortcuts that appears when the app icon is long pressed.",
"use": false,
"dependencies": [
"Settings for YouTube",
@@ -1861,7 +1913,8 @@
"BytecodePatch",
"ResourcePatch",
"ResourcePatch",
- "Settings for YouTube"
+ "Settings for YouTube",
+ "ResourcePatch"
],
"compatiblePackages": {
"com.google.android.youtube": [
@@ -1891,7 +1944,7 @@
{
"key": "bottomMargin",
"title": "Bottom margin",
- "description": "The bottom margin for the overlay buttons and timestamp.",
+ "description": "The bottom margin for the overlay buttons and timestamp. Supports from YouTube 18.29.38 to YouTube 19.16.39.",
"required": true,
"type": "kotlin.String",
"default": "2.5dip",
@@ -1904,7 +1957,7 @@
{
"key": "widerButtonsSpace",
"title": "Wider between-buttons space",
- "description": "Prevent adjacent button presses by increasing the horizontal spacing between buttons.",
+ "description": "Prevent adjacent button presses by increasing the horizontal spacing between buttons. Supports from YouTube 18.29.38 to YouTube 19.16.39.",
"required": true,
"type": "kotlin.Boolean",
"default": false,
@@ -2530,7 +2583,8 @@
"use": true,
"dependencies": [
"BytecodePatch",
- "Settings for YouTube Music"
+ "Settings for YouTube Music",
+ "BytecodePatch"
],
"compatiblePackages": {
"com.google.android.apps.youtube.music": [
@@ -2651,7 +2705,7 @@
},
{
"name": "Toolbar components",
- "description": "Adds options to hide or change components located on the toolbar, such as toolbar buttons, search bar, and header.",
+ "description": "Adds options to hide or change components located on the toolbar, such as the search bar, header, and toolbar buttons.",
"use": true,
"dependencies": [
"BytecodePatch",
diff --git a/patches/api/patches.api b/patches/api/patches.api
index d19702041..440ced4f9 100644
--- a/patches/api/patches.api
+++ b/patches/api/patches.api
@@ -22,10 +22,6 @@ public final class app/revanced/patches/music/flyoutmenu/components/FlyoutMenuCo
public static final fun getFlyoutMenuComponentsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
-public final class app/revanced/patches/music/general/amoled/AmoledPatchKt {
- public static final fun getAmoledPatch ()Lapp/revanced/patcher/patch/ResourcePatch;
-}
-
public final class app/revanced/patches/music/general/autocaptions/AutoCaptionsPatchKt {
public static final fun getAutoCaptionsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
@@ -82,6 +78,11 @@ public final class app/revanced/patches/music/layout/playeroverlay/PlayerOverlay
public static final fun getPlayerOverlayFilterPatch ()Lapp/revanced/patcher/patch/ResourcePatch;
}
+public final class app/revanced/patches/music/layout/theme/DarkThemePatchKt {
+ public static final fun getDARK_COLOR ()[Ljava/lang/String;
+ public static final fun getDarkThemePatch ()Lapp/revanced/patcher/patch/ResourcePatch;
+}
+
public final class app/revanced/patches/music/layout/translations/TranslationsPatchKt {
public static final fun getTranslationsPatch ()Lapp/revanced/patcher/patch/ResourcePatch;
}
@@ -90,6 +91,10 @@ public final class app/revanced/patches/music/layout/visual/VisualPreferencesIco
public static final fun getVisualPreferencesIconsPatch ()Lapp/revanced/patcher/patch/ResourcePatch;
}
+public final class app/revanced/patches/music/misc/album/AlbumMusicVideoPatchKt {
+ public static final fun getAlbumMusicVideoPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
+}
+
public final class app/revanced/patches/music/misc/backgroundplayback/BackgroundPlaybackPatchKt {
public static final fun getBackgroundPlaybackPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
@@ -138,6 +143,10 @@ public final class app/revanced/patches/music/player/components/PlayerComponents
public static final fun getPlayerComponentsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
+public final class app/revanced/patches/music/utils/dismiss/DismissQueueHookPatchKt {
+ public static final fun getDismissQueueHookPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
+}
+
public final class app/revanced/patches/music/utils/extension/SharedExtensionPatchKt {
public static final fun getSharedExtensionPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
@@ -159,6 +168,7 @@ public final class app/revanced/patches/music/utils/fix/fileprovider/FileProvide
}
public final class app/revanced/patches/music/utils/fix/streamingdata/SpoofStreamingDataPatchKt {
+ public static final field EXTENSION_CLASS_DESCRIPTOR Ljava/lang/String;
public static final fun getSpoofStreamingDataPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
@@ -183,6 +193,7 @@ public final class app/revanced/patches/music/utils/playservice/VersionCheckPatc
public static final fun is_6_27_or_greater ()Z
public static final fun is_6_36_or_greater ()Z
public static final fun is_6_42_or_greater ()Z
+ public static final fun is_7_03_or_greater ()Z
public static final fun is_7_06_or_greater ()Z
public static final fun is_7_13_or_greater ()Z
public static final fun is_7_17_or_greater ()Z
@@ -194,6 +205,8 @@ public final class app/revanced/patches/music/utils/playservice/VersionCheckPatc
public final class app/revanced/patches/music/utils/resourceid/SharedResourceIdPatchKt {
public static final fun getAccountSwitcherAccessibility ()J
+ public static final fun getActionBarLogo ()J
+ public static final fun getActionBarLogoRingo2 ()J
public static final fun getBottomSheetRecyclerView ()J
public static final fun getButtonContainer ()J
public static final fun getButtonIconPaddingMedium ()J
@@ -201,6 +214,7 @@ public final class app/revanced/patches/music/utils/resourceid/SharedResourceIdP
public static final fun getColorGrey ()J
public static final fun getDarkBackground ()J
public static final fun getDesignBottomSheetDialog ()J
+ public static final fun getElementsContainer ()J
public static final fun getEndButtonsContainer ()J
public static final fun getFloatingLayout ()J
public static final fun getHistoryMenuItem ()J
@@ -233,6 +247,8 @@ public final class app/revanced/patches/music/utils/resourceid/SharedResourceIdP
public static final fun getTouchOutside ()J
public static final fun getTrimSilenceSwitch ()J
public static final fun getVarispeedUnavailableTitle ()J
+ public static final fun getYtmLogo ()J
+ public static final fun getYtmLogoRingo2 ()J
public static final fun isTablet ()J
}
@@ -274,6 +290,12 @@ public final class app/revanced/patches/music/video/playback/VideoPlaybackPatchK
public static final fun getVideoPlaybackPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
+public final class app/revanced/patches/music/video/playerresponse/PlayerResponseMethodHookPatchKt {
+ public static final fun getPlayerResponseMethodHookPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
+ public static final fun hookPlayerResponse (Ljava/lang/String;Z)V
+ public static synthetic fun hookPlayerResponse$default (Ljava/lang/String;ZILjava/lang/Object;)V
+}
+
public final class app/revanced/patches/reddit/ad/AdsPatchKt {
public static final fun getAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
@@ -428,6 +450,8 @@ public final class app/revanced/patches/shared/mainactivity/BaseMainActivityReso
public static final fun getMainActivityMutableClass ()Lapp/revanced/patcher/util/proxy/mutableTypes/MutableClass;
public static final fun getOnConfigurationChangedMethod ()Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;
public static final fun getOnCreateMethod ()Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;
+ public static final fun getOnStartMethod ()Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;
+ public static final fun getOnStopMethod ()Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;
}
public final class app/revanced/patches/shared/mapping/ResourceElement {
@@ -469,6 +493,10 @@ public final class app/revanced/patches/shared/mapping/ResourceType : java/lang/
public static fun values ()[Lapp/revanced/patches/shared/mapping/ResourceType;
}
+public final class app/revanced/patches/shared/materialyou/BaseMaterialYouPatchKt {
+ public static final fun baseMaterialYou (Lapp/revanced/patcher/patch/ResourcePatchContext;)V
+}
+
public final class app/revanced/patches/shared/opus/BaseOpusCodecsPatchKt {
public static final fun baseOpusCodecsPatch (Ljava/lang/String;)Lapp/revanced/patcher/patch/BytecodePatch;
}
@@ -830,6 +858,10 @@ public final class app/revanced/patches/youtube/utils/flyoutmenu/FlyoutMenuHookP
public static final fun getFlyoutMenuHookPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
+public final class app/revanced/patches/youtube/utils/fullscreen/FullscreenButtonHookPatchKt {
+ public static final fun getFullscreenButtonHookPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
+}
+
public final class app/revanced/patches/youtube/utils/gms/GmsCoreSupportPatchKt {
public static final fun getGmsCoreSupportPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
@@ -1150,6 +1182,7 @@ public final class app/revanced/util/BytecodeUtilsKt {
public static final fun or (Lcom/android/tools/smali/dexlib2/AccessFlags;I)I
public static final fun or (Lcom/android/tools/smali/dexlib2/AccessFlags;Lcom/android/tools/smali/dexlib2/AccessFlags;)I
public static final fun parametersEqual (Ljava/lang/Iterable;Ljava/lang/Iterable;)Z
+ public static final fun replaceLiteralInstructionCall (Lapp/revanced/patcher/patch/BytecodePatchContext;JJ)V
public static final fun replaceLiteralInstructionCall (Lapp/revanced/patcher/patch/BytecodePatchContext;JLjava/lang/String;)V
public static final fun returnEarly (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;Z)V
public static synthetic fun returnEarly$default (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;ZILjava/lang/Object;)V
@@ -1178,6 +1211,7 @@ public final class app/revanced/util/ResourceUtilsKt {
public static final fun copyXmlNode (Lapp/revanced/patcher/patch/ResourcePatchContext;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lkotlin/Unit;
public static final fun copyXmlNode (Ljava/lang/String;Lapp/revanced/patcher/util/Document;Lapp/revanced/patcher/util/Document;)Ljava/lang/AutoCloseable;
public static final fun doRecursively (Lorg/w3c/dom/Node;Lkotlin/jvm/functions/Function1;)V
+ public static final fun getBooleanOptionValue (Lapp/revanced/patcher/patch/Patch;Ljava/lang/String;)Lapp/revanced/patcher/patch/Option;
public static final fun getResourceGroup (Ljava/util/List;[Ljava/lang/String;)Ljava/util/List;
public static final fun getStringOptionValue (Lapp/revanced/patcher/patch/Patch;Ljava/lang/String;)Lapp/revanced/patcher/patch/Option;
public static final fun insertNode (Lorg/w3c/dom/Node;Ljava/lang/String;Lorg/w3c/dom/Node;Lkotlin/jvm/functions/Function1;)V
diff --git a/patches/src/main/kotlin/app/revanced/generator/JsonPatchesFileGenerator.kt b/patches/src/main/kotlin/app/revanced/generator/JsonPatchesFileGenerator.kt
index 2e916af51..3a7b29342 100644
--- a/patches/src/main/kotlin/app/revanced/generator/JsonPatchesFileGenerator.kt
+++ b/patches/src/main/kotlin/app/revanced/generator/JsonPatchesFileGenerator.kt
@@ -30,7 +30,9 @@ internal class JsonPatchesFileGenerator : PatchesFileGenerator {
},
)
}.let {
- patchesJson.writeText(GsonBuilder().serializeNulls().setPrettyPrinting().create().toJson(it))
+ patchesJson.writeText(
+ GsonBuilder().serializeNulls().setPrettyPrinting().create().toJson(it)
+ )
}
}
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/general/amoled/AmoledPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/general/amoled/AmoledPatch.kt
deleted file mode 100644
index c2a79075d..000000000
--- a/patches/src/main/kotlin/app/revanced/patches/music/general/amoled/AmoledPatch.kt
+++ /dev/null
@@ -1,46 +0,0 @@
-package app.revanced.patches.music.general.amoled
-
-import app.revanced.patcher.patch.resourcePatch
-import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE
-import app.revanced.patches.music.utils.extension.Constants.UTILS_PATH
-import app.revanced.patches.music.utils.patch.PatchList.AMOLED
-import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus
-import app.revanced.patches.music.utils.settings.settingsPatch
-import app.revanced.patches.shared.drawable.addDrawableColorHook
-import app.revanced.patches.shared.drawable.drawableColorHookPatch
-import org.w3c.dom.Element
-
-@Suppress("unused")
-val amoledPatch = resourcePatch(
- AMOLED.title,
- AMOLED.summary,
-) {
- compatibleWith(COMPATIBLE_PACKAGE)
-
- dependsOn(
- drawableColorHookPatch,
- settingsPatch
- )
-
- execute {
- addDrawableColorHook("$UTILS_PATH/DrawableColorPatch;->getLithoColor(I)I")
-
- document("res/values/colors.xml").use { document ->
- val resourcesNode = document.getElementsByTagName("resources").item(0) as Element
-
- for (i in 0 until resourcesNode.childNodes.length) {
- val node = resourcesNode.childNodes.item(i) as? Element ?: continue
-
- node.textContent = when (node.getAttribute("name")) {
- "yt_black0", "yt_black1", "yt_black1_opacity95", "yt_black1_opacity98", "yt_black2", "yt_black3",
- "yt_black4", "yt_status_bar_background_dark", "ytm_color_grey_12", "material_grey_850" -> "@android:color/black"
-
- else -> continue
- }
- }
- }
-
- updatePatchStatus(AMOLED)
-
- }
-}
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/layout/header/ChangeHeaderPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/layout/header/ChangeHeaderPatch.kt
index db08f642f..8143cfddd 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/layout/header/ChangeHeaderPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/layout/header/ChangeHeaderPatch.kt
@@ -5,6 +5,13 @@ import app.revanced.patcher.patch.resourcePatch
import app.revanced.patcher.patch.stringOption
import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE
import app.revanced.patches.music.utils.patch.PatchList.CUSTOM_HEADER_FOR_YOUTUBE_MUSIC
+import app.revanced.patches.music.utils.playservice.is_7_06_or_greater
+import app.revanced.patches.music.utils.playservice.versionCheckPatch
+import app.revanced.patches.music.utils.resourceid.actionBarLogo
+import app.revanced.patches.music.utils.resourceid.actionBarLogoRingo2
+import app.revanced.patches.music.utils.resourceid.sharedResourceIdPatch
+import app.revanced.patches.music.utils.resourceid.ytmLogo
+import app.revanced.patches.music.utils.resourceid.ytmLogoRingo2
import app.revanced.patches.music.utils.settings.ResourceUtils.getIconType
import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus
import app.revanced.patches.music.utils.settings.settingsPatch
@@ -13,8 +20,7 @@ import app.revanced.util.Utils.printWarn
import app.revanced.util.Utils.trimIndentMultiline
import app.revanced.util.copyFile
import app.revanced.util.copyResources
-import app.revanced.util.fingerprint.injectLiteralInstructionBooleanCall
-import app.revanced.util.fingerprint.resolvable
+import app.revanced.util.replaceLiteralInstructionCall
import app.revanced.util.underBarOrThrow
import app.revanced.util.valueOrThrow
@@ -100,24 +106,31 @@ private val getDescription = {
private val changeHeaderBytecodePatch = bytecodePatch(
description = "changeHeaderBytecodePatch"
) {
+ dependsOn(
+ sharedResourceIdPatch,
+ versionCheckPatch,
+ )
+
execute {
+
/**
* New Header has been added from YouTube Music v7.04.51.
*
- * The new header's file names are 'action_bar_logo_ringo2.png' and 'ytm_logo_ringo2.png'.
+ * The new header's file names are 'action_bar_logo_ringo2.png' and 'ytm_logo_ringo2.png'.
* The only difference between the existing header and the new header is the dimensions of the image.
*
* The affected patch is [changeHeaderPatch].
- *
- * TODO: Add a new header image file to [changeHeaderPatch] later.
*/
- if (!headerSwitchConfigFingerprint.resolvable()) {
+ if (!is_7_06_or_greater) {
return@execute
}
- headerSwitchConfigFingerprint.injectLiteralInstructionBooleanCall(
- 45617851L,
- "0x0"
- )
+
+ listOf(
+ actionBarLogoRingo2 to actionBarLogo,
+ ytmLogoRingo2 to ytmLogo,
+ ).forEach { (originalResource, replacementResource) ->
+ replaceLiteralInstructionCall(originalResource, replacementResource)
+ }
}
}
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/layout/header/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/music/layout/header/Fingerprints.kt
deleted file mode 100644
index 866303d2f..000000000
--- a/patches/src/main/kotlin/app/revanced/patches/music/layout/header/Fingerprints.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-package app.revanced.patches.music.layout.header
-
-import app.revanced.util.fingerprint.legacyFingerprint
-import app.revanced.util.or
-import com.android.tools.smali.dexlib2.AccessFlags
-
-internal val headerSwitchConfigFingerprint = legacyFingerprint(
- name = "headerSwitchConfigFingerprint",
- returnType = "Z",
- accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
- literals = listOf(45617851L)
-)
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/layout/theme/DarkThemePatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/layout/theme/DarkThemePatch.kt
new file mode 100644
index 000000000..45f85b967
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/music/layout/theme/DarkThemePatch.kt
@@ -0,0 +1,153 @@
+package app.revanced.patches.music.layout.theme
+
+import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
+import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
+import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
+import app.revanced.patcher.patch.booleanOption
+import app.revanced.patcher.patch.bytecodePatch
+import app.revanced.patcher.patch.resourcePatch
+import app.revanced.patcher.patch.stringOption
+import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE
+import app.revanced.patches.music.utils.extension.Constants.PATCH_STATUS_CLASS_DESCRIPTOR
+import app.revanced.patches.music.utils.extension.Constants.UTILS_PATH
+import app.revanced.patches.music.utils.patch.PatchList.DARK_THEME
+import app.revanced.patches.music.utils.resourceid.sharedResourceIdPatch
+import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus
+import app.revanced.patches.music.utils.settings.settingsPatch
+import app.revanced.patches.shared.drawable.addDrawableColorHook
+import app.revanced.patches.shared.drawable.drawableColorHookPatch
+import app.revanced.patches.shared.materialyou.baseMaterialYou
+import app.revanced.util.ResourceGroup
+import app.revanced.util.copyResources
+import app.revanced.util.findMethodOrThrow
+import app.revanced.util.fingerprint.methodOrThrow
+import app.revanced.util.indexOfFirstInstructionReversedOrThrow
+import app.revanced.util.valueOrThrow
+import com.android.tools.smali.dexlib2.Opcode
+import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
+import org.w3c.dom.Element
+
+private const val EXTENSION_CLASS_DESCRIPTOR =
+ "$UTILS_PATH/DrawableColorPatch;"
+
+private val darkThemeBytecodePatch = bytecodePatch(
+ description = "darkThemeBytecodePatch"
+) {
+ dependsOn(
+ settingsPatch,
+ sharedResourceIdPatch,
+ drawableColorHookPatch,
+ )
+
+ execute {
+ addDrawableColorHook("$EXTENSION_CLASS_DESCRIPTOR->getLithoColor(I)I")
+
+ // The images in the playlist and album headers have a black gradient (probably applied server-side).
+ // Applies a new gradient to the images in the playlist and album headers.
+ elementsContainerFingerprint.methodOrThrow().apply {
+ val index = indexOfFirstInstructionReversedOrThrow(Opcode.CHECK_CAST)
+ val register = getInstruction(index).registerA
+
+ addInstruction(
+ index + 1,
+ "invoke-static {v$register}, $EXTENSION_CLASS_DESCRIPTOR->setHeaderGradient(Landroid/view/ViewGroup;)V"
+ )
+ }
+
+ findMethodOrThrow(PATCH_STATUS_CLASS_DESCRIPTOR) {
+ name == "DarkTheme"
+ }.replaceInstruction(
+ 0,
+ "const/4 v0, 0x1"
+ )
+ }
+}
+
+val DARK_COLOR = arrayOf(
+ "yt_black0", "yt_black1", "yt_black1_opacity95", "yt_black1_opacity98",
+ "yt_black2", "yt_black3", "yt_black4", "yt_black_pure",
+ "yt_black_pure_opacity80", "yt_status_bar_background_dark",
+ "ytm_color_grey_12", "material_grey_800", "material_grey_850",
+)
+
+@Suppress("unused")
+val darkThemePatch = resourcePatch(
+ DARK_THEME.title,
+ DARK_THEME.summary,
+) {
+ compatibleWith(COMPATIBLE_PACKAGE)
+
+ dependsOn(darkThemeBytecodePatch)
+
+ val amoledBlackColor = "@android:color/black"
+
+ val darkThemeBackgroundColor = stringOption(
+ key = "darkThemeBackgroundColor",
+ default = amoledBlackColor,
+ values = mapOf(
+ "Amoled Black" to amoledBlackColor,
+ "Catppuccin (Mocha)" to "#FF181825",
+ "Dark Pink" to "#FF290025",
+ "Dark Blue" to "#FF001029",
+ "Dark Green" to "#FF002905",
+ "Dark Yellow" to "#FF282900",
+ "Dark Orange" to "#FF291800",
+ "Dark Red" to "#FF290000",
+ ),
+ title = "Dark theme background color",
+ description = "Can be a hex color (#AARRGGBB) or a color resource reference.",
+ )
+
+ val materialYou by booleanOption(
+ key = "materialYou",
+ default = false,
+ title = "MaterialYou",
+ description = "Applies the MaterialYou theme for Android 12+ devices.",
+ required = true
+ )
+
+ execute {
+ // Check patch options first.
+ val darkThemeColor = darkThemeBackgroundColor
+ .valueOrThrow()
+
+ document("res/values/colors.xml").use { document ->
+ val resourcesNode = document.getElementsByTagName("resources").item(0) as Element
+
+ for (i in 0 until resourcesNode.childNodes.length) {
+ val node = resourcesNode.childNodes.item(i) as? Element ?: continue
+ val colorName = node.getAttribute("name")
+
+ if (DARK_COLOR.contains(colorName)) {
+ node.textContent = darkThemeColor
+ }
+ }
+ }
+
+ arrayOf(
+ ResourceGroup(
+ "drawable",
+ "revanced_header_gradient.xml",
+ )
+ ).forEach { resourceGroup ->
+ copyResources("music/theme", resourceGroup)
+ }
+
+ if (materialYou == true) {
+ baseMaterialYou()
+
+ document("res/values-v31/colors.xml").use { document ->
+ DARK_COLOR.forEach { name ->
+ val colorElement = document.createElement("color")
+ colorElement.setAttribute("name", name)
+ colorElement.textContent = "@android:color/system_neutral1_900"
+
+ document.getElementsByTagName("resources").item(0).appendChild(colorElement)
+ }
+ }
+ }
+
+ updatePatchStatus(DARK_THEME)
+
+ }
+}
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/layout/theme/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/music/layout/theme/Fingerprints.kt
new file mode 100644
index 000000000..7b5a00b94
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/music/layout/theme/Fingerprints.kt
@@ -0,0 +1,15 @@
+package app.revanced.patches.music.layout.theme
+
+import app.revanced.patches.music.utils.resourceid.elementsContainer
+import app.revanced.util.fingerprint.legacyFingerprint
+import app.revanced.util.or
+import com.android.tools.smali.dexlib2.AccessFlags
+import com.android.tools.smali.dexlib2.Opcode
+
+internal val elementsContainerFingerprint = legacyFingerprint(
+ name = "elementsContainerFingerprint",
+ returnType = "V",
+ accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR,
+ opcodes = listOf(Opcode.INVOKE_DIRECT_RANGE),
+ literals = listOf(elementsContainer)
+)
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/misc/album/AlbumMusicVideoPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/album/AlbumMusicVideoPatch.kt
new file mode 100644
index 000000000..f72a914ee
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/album/AlbumMusicVideoPatch.kt
@@ -0,0 +1,125 @@
+package app.revanced.patches.music.misc.album
+
+import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
+import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels
+import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
+import app.revanced.patcher.patch.bytecodePatch
+import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE
+import app.revanced.patches.music.utils.extension.Constants.MISC_PATH
+import app.revanced.patches.music.utils.patch.PatchList.DISABLE_MUSIC_VIDEO_IN_ALBUM
+import app.revanced.patches.music.utils.settings.CategoryType
+import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus
+import app.revanced.patches.music.utils.settings.addPreferenceWithIntent
+import app.revanced.patches.music.utils.settings.addSwitchPreference
+import app.revanced.patches.music.utils.settings.settingsPatch
+import app.revanced.patches.music.video.information.videoIdHook
+import app.revanced.patches.music.video.information.videoInformationPatch
+import app.revanced.patches.music.video.playerresponse.hookPlayerResponse
+import app.revanced.patches.music.video.playerresponse.playerResponseMethodHookPatch
+import app.revanced.util.findMethodOrThrow
+import app.revanced.util.fingerprint.methodOrThrow
+import app.revanced.util.getReference
+import app.revanced.util.indexOfFirstInstructionReversedOrThrow
+import com.android.tools.smali.dexlib2.Opcode
+import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction
+import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
+import com.android.tools.smali.dexlib2.iface.reference.MethodReference
+
+private const val EXTENSION_CLASS_DESCRIPTOR =
+ "$MISC_PATH/AlbumMusicVideoPatch;"
+
+@Suppress("unused")
+val albumMusicVideoPatch = bytecodePatch(
+ DISABLE_MUSIC_VIDEO_IN_ALBUM.title,
+ DISABLE_MUSIC_VIDEO_IN_ALBUM.summary,
+ false,
+) {
+ compatibleWith(COMPATIBLE_PACKAGE)
+
+ dependsOn(
+ settingsPatch,
+ videoInformationPatch,
+ playerResponseMethodHookPatch,
+ )
+
+ execute {
+
+ // region hook player response
+
+ hookPlayerResponse("$EXTENSION_CLASS_DESCRIPTOR->newPlayerResponse(Ljava/lang/String;Ljava/lang/String;I)V")
+
+ // endregion
+
+ // region hook video id
+
+ videoIdHook("$EXTENSION_CLASS_DESCRIPTOR->newVideoLoaded(Ljava/lang/String;)V")
+
+ // endregion
+
+ // region patch for hide snack bar
+
+ snackBarParentFingerprint.methodOrThrow().addInstructionsWithLabels(
+ 0, """
+ invoke-static {}, $EXTENSION_CLASS_DESCRIPTOR->hideSnackBar()Z
+ move-result v0
+ if-eqz v0, :hide
+ return-void
+ :hide
+ nop
+ """
+ )
+
+ // endregion
+
+ // region patch for setOnClick / setOnLongClick listener
+
+ audioVideoSwitchToggleConstructorFingerprint.methodOrThrow().apply {
+ val onClickListenerIndex = indexOfAudioVideoSwitchSetOnClickListenerInstruction(this)
+ val viewRegister =
+ getInstruction(onClickListenerIndex).registerC
+
+ addInstruction(
+ onClickListenerIndex + 1,
+ "invoke-static { v$viewRegister }, " +
+ "$EXTENSION_CLASS_DESCRIPTOR->setAudioVideoSwitchToggleOnLongClickListener(Landroid/view/View;)V"
+ )
+
+ val onClickListenerSyntheticIndex =
+ indexOfFirstInstructionReversedOrThrow(onClickListenerIndex) {
+ opcode == Opcode.INVOKE_DIRECT &&
+ getReference()?.name == ""
+ }
+ val onClickListenerSyntheticClass =
+ (getInstruction(onClickListenerSyntheticIndex).reference as MethodReference).definingClass
+
+ findMethodOrThrow(onClickListenerSyntheticClass) {
+ name == "onClick"
+ }.addInstructionsWithLabels(
+ 0, """
+ invoke-static {}, $EXTENSION_CLASS_DESCRIPTOR->openMusic()Z
+ move-result v0
+ if-eqz v0, :ignore
+ return-void
+ :ignore
+ nop
+ """
+ )
+ }
+
+ // endregion
+
+ addSwitchPreference(
+ CategoryType.MISC,
+ "revanced_disable_music_video_in_album",
+ "false"
+ )
+ addPreferenceWithIntent(
+ CategoryType.MISC,
+ "revanced_disable_music_video_in_album_redirect_type",
+ "revanced_disable_music_video_in_album"
+ )
+
+ updatePatchStatus(DISABLE_MUSIC_VIDEO_IN_ALBUM)
+
+ }
+}
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/misc/album/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/album/Fingerprints.kt
new file mode 100644
index 000000000..52f0bf163
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/album/Fingerprints.kt
@@ -0,0 +1,34 @@
+package app.revanced.patches.music.misc.album
+
+import app.revanced.util.fingerprint.legacyFingerprint
+import app.revanced.util.getReference
+import app.revanced.util.indexOfFirstInstruction
+import app.revanced.util.or
+import com.android.tools.smali.dexlib2.AccessFlags
+import com.android.tools.smali.dexlib2.Opcode
+import com.android.tools.smali.dexlib2.iface.Method
+import com.android.tools.smali.dexlib2.iface.reference.MethodReference
+
+internal val audioVideoSwitchToggleConstructorFingerprint = legacyFingerprint(
+ name = "audioVideoSwitchToggleConstructorFingerprint",
+ accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR,
+ returnType = "V",
+ opcodes = listOf(Opcode.INVOKE_DIRECT),
+ customFingerprint = { method, _ ->
+ indexOfAudioVideoSwitchSetOnClickListenerInstruction(method) >= 0
+ }
+)
+
+internal fun indexOfAudioVideoSwitchSetOnClickListenerInstruction(method: Method) =
+ method.indexOfFirstInstruction {
+ opcode == Opcode.INVOKE_VIRTUAL &&
+ getReference()?.toString() == "Lcom/google/android/apps/youtube/music/player/AudioVideoSwitcherToggleView;->setOnClickListener(Landroid/view/View${'$'}OnClickListener;)V"
+ }
+
+internal val snackBarParentFingerprint = legacyFingerprint(
+ name = "snackBarParentFingerprint",
+ accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
+ returnType = "V",
+ parameters = listOf("L"),
+ strings = listOf("No suitable parent found from the given view. Please provide a valid view.")
+)
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/player/components/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/music/player/components/Fingerprints.kt
index ea0046e1b..bf5204063 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/player/components/Fingerprints.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/player/components/Fingerprints.kt
@@ -11,6 +11,7 @@ import app.revanced.patches.music.utils.resourceid.miniPlayerViewPager
import app.revanced.patches.music.utils.resourceid.playerViewPager
import app.revanced.patches.music.utils.resourceid.remixGenericButtonSize
import app.revanced.patches.music.utils.resourceid.tapBloomView
+import app.revanced.util.containsLiteralInstruction
import app.revanced.util.fingerprint.legacyFingerprint
import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstruction
@@ -50,10 +51,11 @@ internal val engagementPanelHeightFingerprint = legacyFingerprint(
),
parameters = emptyList(),
customFingerprint = { method, _ ->
- method.indexOfFirstInstruction {
- opcode == Opcode.INVOKE_VIRTUAL &&
- getReference()?.name == "booleanValue"
- } >= 0
+ method.containsLiteralInstruction(1) &&
+ method.indexOfFirstInstruction {
+ opcode == Opcode.INVOKE_VIRTUAL &&
+ getReference()?.name == "booleanValue"
+ } >= 0
}
)
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/utils/dismiss/DismissQueueHookPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/utils/dismiss/DismissQueueHookPatch.kt
new file mode 100644
index 000000000..7cb76868f
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/dismiss/DismissQueueHookPatch.kt
@@ -0,0 +1,42 @@
+package app.revanced.patches.music.utils.dismiss
+
+import app.revanced.patcher.patch.bytecodePatch
+import app.revanced.patches.music.utils.extension.Constants.EXTENSION_PATH
+import app.revanced.util.addStaticFieldToExtension
+import app.revanced.util.fingerprint.methodOrThrow
+import app.revanced.util.getWalkerMethod
+
+private const val EXTENSION_VIDEO_UTILS_CLASS_DESCRIPTOR =
+ "$EXTENSION_PATH/utils/VideoUtils;"
+
+@Suppress("unused")
+val dismissQueueHookPatch = bytecodePatch(
+ description = "dismissQueueHookPatch"
+) {
+
+ execute {
+
+ dismissQueueFingerprint.methodOrThrow().apply {
+ val dismissQueueIndex = indexOfDismissQueueInstruction(this)
+
+ getWalkerMethod(dismissQueueIndex).apply {
+ val smaliInstructions =
+ """
+ if-eqz v0, :ignore
+ invoke-virtual {v0}, $definingClass->$name()V
+ :ignore
+ return-void
+ """
+
+ addStaticFieldToExtension(
+ EXTENSION_VIDEO_UTILS_CLASS_DESCRIPTOR,
+ "dismissQueue",
+ "dismissQueueClass",
+ definingClass,
+ smaliInstructions
+ )
+ }
+ }
+
+ }
+}
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/utils/dismiss/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/music/utils/dismiss/Fingerprints.kt
new file mode 100644
index 000000000..49c00a3b7
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/dismiss/Fingerprints.kt
@@ -0,0 +1,24 @@
+package app.revanced.patches.music.utils.dismiss
+
+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 dismissQueueFingerprint = legacyFingerprint(
+ name = "dismissQueueFingerprint",
+ returnType = "V",
+ parameters = listOf("L"),
+ customFingerprint = { method, _ ->
+ method.name == "handleDismissWatchEvent" &&
+ indexOfDismissQueueInstruction(method) >= 0
+ }
+)
+
+internal fun indexOfDismissQueueInstruction(method: Method) =
+ method.indexOfFirstInstruction {
+ opcode == Opcode.INVOKE_VIRTUAL &&
+ getReference()?.definingClass?.endsWith("/MppWatchWhileLayout;") == true
+ }
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/utils/fix/streamingdata/SpoofStreamingDataPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/utils/fix/streamingdata/SpoofStreamingDataPatch.kt
index a37eeeeb8..a30c435c2 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/utils/fix/streamingdata/SpoofStreamingDataPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/fix/streamingdata/SpoofStreamingDataPatch.kt
@@ -1,5 +1,6 @@
package app.revanced.patches.music.utils.fix.streamingdata
+import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE
import app.revanced.patches.music.utils.compatibility.Constants.YOUTUBE_MUSIC_PACKAGE_NAME
import app.revanced.patches.music.utils.patch.PatchList.SPOOF_STREAMING_DATA
@@ -8,8 +9,16 @@ import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus
import app.revanced.patches.music.utils.settings.addPreferenceWithIntent
import app.revanced.patches.music.utils.settings.addSwitchPreference
import app.revanced.patches.music.utils.settings.settingsPatch
+import app.revanced.patches.music.video.playerresponse.hookPlayerResponse
+import app.revanced.patches.music.video.playerresponse.playerResponseMethodHookPatch
+import app.revanced.patches.shared.extension.Constants.PATCHES_PATH
+import app.revanced.patches.shared.extension.Constants.SPOOF_PATH
import app.revanced.patches.shared.spoof.streamingdata.baseSpoofStreamingDataPatch
import app.revanced.patches.shared.spoof.useragent.baseSpoofUserAgentPatch
+import app.revanced.util.findMethodOrThrow
+
+const val EXTENSION_CLASS_DESCRIPTOR =
+ "$SPOOF_PATH/SpoofStreamingDataPatch;"
@Suppress("unused")
val spoofStreamingDataPatch = baseSpoofStreamingDataPatch(
@@ -19,9 +28,22 @@ val spoofStreamingDataPatch = baseSpoofStreamingDataPatch(
dependsOn(
baseSpoofUserAgentPatch(YOUTUBE_MUSIC_PACKAGE_NAME),
settingsPatch,
+ playerResponseMethodHookPatch,
)
},
{
+ findMethodOrThrow("$PATCHES_PATH/PatchStatus;") {
+ name == "SpoofStreamingDataMusic"
+ }.replaceInstruction(
+ 0,
+ "const/4 v0, 0x1"
+ )
+
+ hookPlayerResponse(
+ "$EXTENSION_CLASS_DESCRIPTOR->fetchStreams(Ljava/lang/String;)V",
+ true
+ )
+
addSwitchPreference(
CategoryType.MISC,
"revanced_spoof_streaming_data",
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/utils/patch/PatchList.kt b/patches/src/main/kotlin/app/revanced/patches/music/utils/patch/PatchList.kt
index c814a7d97..11d02b637 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/utils/patch/PatchList.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/patch/PatchList.kt
@@ -5,10 +5,6 @@ internal enum class PatchList(
val summary: String,
var included: Boolean? = false
) {
- AMOLED(
- "Amoled",
- "Applies a pure black theme to some components."
- ),
BITRATE_DEFAULT_VALUE(
"Bitrate default value",
"Sets the audio quality to 'Always High' when you first install the app."
@@ -23,7 +19,7 @@ internal enum class PatchList(
),
CHANGE_SHARE_SHEET(
"Change share sheet",
- "Add option to change from in-app share sheet to system share sheet."
+ "Adds an option to change the in-app share sheet to the system share sheet."
),
CHANGE_START_PAGE(
"Change start page",
@@ -41,6 +37,10 @@ internal enum class PatchList(
"Custom header for YouTube Music",
"Applies a custom header in the top left corner within the app."
),
+ DARK_THEME(
+ "Dark theme",
+ "Changes the app's dark theme to the values specified in patch options."
+ ),
DISABLE_CAIRO_SPLASH_ANIMATION(
"Disable Cairo splash animation",
"Adds an option to disable Cairo splash animation."
@@ -57,6 +57,10 @@ internal enum class PatchList(
"Disable dislike redirection",
"Adds an option to disable redirection to the next track when clicking the Dislike button."
),
+ DISABLE_MUSIC_VIDEO_IN_ALBUM(
+ "Disable music video in album",
+ "Adds option to redirect music videos from albums for non-premium users."
+ ),
ENABLE_OPUS_CODEC(
"Enable OPUS codec",
"Adds an options to enable the OPUS audio codec if the player response includes."
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/utils/playservice/VersionCheckPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/utils/playservice/VersionCheckPatch.kt
index 372251b2b..6344ff17a 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/utils/playservice/VersionCheckPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/playservice/VersionCheckPatch.kt
@@ -11,6 +11,8 @@ var is_6_36_or_greater = false
private set
var is_6_42_or_greater = false
private set
+var is_7_03_or_greater = false
+ private set
var is_7_06_or_greater = false
private set
var is_7_13_or_greater = false
@@ -43,6 +45,7 @@ val versionCheckPatch = resourcePatch(
is_6_27_or_greater = 234412000 <= playStoreServicesVersion
is_6_36_or_greater = 240399000 <= playStoreServicesVersion
is_6_42_or_greater = 240999000 <= playStoreServicesVersion
+ is_7_03_or_greater = 242199000 <= playStoreServicesVersion
is_7_06_or_greater = 242499000 <= playStoreServicesVersion
is_7_13_or_greater = 243199000 <= playStoreServicesVersion
is_7_17_or_greater = 243530000 <= playStoreServicesVersion
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/utils/resourceid/SharedResourceIdPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/utils/resourceid/SharedResourceIdPatch.kt
index a3358901f..3363e56b0 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/utils/resourceid/SharedResourceIdPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/resourceid/SharedResourceIdPatch.kt
@@ -4,6 +4,7 @@ import app.revanced.patcher.patch.resourcePatch
import app.revanced.patches.shared.mapping.ResourceType.BOOL
import app.revanced.patches.shared.mapping.ResourceType.COLOR
import app.revanced.patches.shared.mapping.ResourceType.DIMEN
+import app.revanced.patches.shared.mapping.ResourceType.DRAWABLE
import app.revanced.patches.shared.mapping.ResourceType.ID
import app.revanced.patches.shared.mapping.ResourceType.LAYOUT
import app.revanced.patches.shared.mapping.ResourceType.STRING
@@ -14,6 +15,10 @@ import app.revanced.patches.shared.mapping.resourceMappings
var accountSwitcherAccessibility = -1L
private set
+var actionBarLogo = -1L
+ private set
+var actionBarLogoRingo2 = -1L
+ private set
var bottomSheetRecyclerView = -1L
private set
var buttonContainer = -1L
@@ -28,6 +33,8 @@ var darkBackground = -1L
private set
var designBottomSheetDialog = -1L
private set
+var elementsContainer = -1L
+ private set
var endButtonsContainer = -1L
private set
var floatingLayout = -1L
@@ -94,6 +101,10 @@ var trimSilenceSwitch = -1L
private set
var varispeedUnavailableTitle = -1L
private set
+var ytmLogo = -1L
+ private set
+var ytmLogoRingo2 = -1L
+ private set
internal val sharedResourceIdPatch = resourcePatch(
description = "sharedResourceIdPatch"
@@ -105,6 +116,14 @@ internal val sharedResourceIdPatch = resourcePatch(
STRING,
"account_switcher_accessibility_label",
]
+ actionBarLogo = resourceMappings[
+ DRAWABLE,
+ "action_bar_logo",
+ ]
+ actionBarLogoRingo2 = resourceMappings[
+ DRAWABLE,
+ "action_bar_logo_ringo2",
+ ]
bottomSheetRecyclerView = resourceMappings[
LAYOUT,
"bottom_sheet_recycler_view"
@@ -133,6 +152,10 @@ internal val sharedResourceIdPatch = resourcePatch(
LAYOUT,
"design_bottom_sheet_dialog"
]
+ elementsContainer = resourceMappings[
+ ID,
+ "elements_container"
+ ]
endButtonsContainer = resourceMappings[
ID,
"end_buttons_container"
@@ -265,5 +288,13 @@ internal val sharedResourceIdPatch = resourcePatch(
STRING,
"varispeed_unavailable_title"
]
+ ytmLogo = resourceMappings[
+ DRAWABLE,
+ "ytm_logo",
+ ]
+ ytmLogoRingo2 = resourceMappings[
+ DRAWABLE,
+ "ytm_logo_ringo2",
+ ]
}
}
\ No newline at end of file
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/video/playerresponse/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/music/video/playerresponse/Fingerprints.kt
new file mode 100644
index 000000000..0564715e1
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/music/video/playerresponse/Fingerprints.kt
@@ -0,0 +1,61 @@
+package app.revanced.patches.music.video.playerresponse
+
+import app.revanced.util.fingerprint.legacyFingerprint
+import app.revanced.util.or
+import com.android.tools.smali.dexlib2.AccessFlags
+import com.android.tools.smali.dexlib2.Opcode
+
+/**
+ * For targets 7.03 and later.
+ */
+internal val playerParameterBuilderFingerprint = legacyFingerprint(
+ name = "playerParameterBuilderFingerprint",
+ accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
+ returnType = "L",
+ parameters = listOf(
+ "Ljava/lang/String;", // VideoId.
+ "[B",
+ "Ljava/lang/String;", // Player parameters proto buffer.
+ "Ljava/lang/String;", // PlaylistId.
+ "I", // PlaylistIndex.
+ "I",
+ "L",
+ "Ljava/util/Set;",
+ "Ljava/lang/String;",
+ "Ljava/lang/String;",
+ "L",
+ "Z",
+ "Z",
+ "Z", // Appears to indicate if the video id is being opened or is currently playing.
+ ),
+ strings = listOf("psps")
+)
+
+/**
+ * For targets 7.02 and earlier.
+ */
+internal val playerParameterBuilderLegacyFingerprint = legacyFingerprint(
+ name = "playerParameterBuilderLegacyFingerprint",
+ accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
+ returnType = "L",
+ parameters = listOf(
+ "Ljava/lang/String;", // VideoId.
+ "[B",
+ "Ljava/lang/String;", // Player parameters proto buffer.
+ "Ljava/lang/String;", // PlaylistId.
+ "I", // PlaylistIndex.
+ "I",
+ "Ljava/util/Set;",
+ "Ljava/lang/String;",
+ "Ljava/lang/String;",
+ "L",
+ "Z",
+ "Z", // Appears to indicate if the video id is being opened or is currently playing.
+ ),
+ opcodes = listOf(
+ Opcode.INVOKE_INTERFACE,
+ Opcode.MOVE_RESULT_OBJECT,
+ Opcode.CHECK_CAST,
+ Opcode.INVOKE_INTERFACE
+ )
+)
\ No newline at end of file
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/video/playerresponse/PlayerResponseMethodHookPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/video/playerresponse/PlayerResponseMethodHookPatch.kt
new file mode 100644
index 000000000..8570097d0
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/music/video/playerresponse/PlayerResponseMethodHookPatch.kt
@@ -0,0 +1,40 @@
+package app.revanced.patches.music.video.playerresponse
+
+import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
+import app.revanced.patcher.patch.bytecodePatch
+import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
+import app.revanced.patches.music.utils.playservice.is_7_03_or_greater
+import app.revanced.patches.music.utils.playservice.versionCheckPatch
+import app.revanced.util.fingerprint.methodOrThrow
+
+private const val REGISTER_VIDEO_ID = "p1"
+private const val REGISTER_PLAYLIST_ID = "p4"
+private const val REGISTER_PLAYLIST_INDEX = "p5"
+
+private lateinit var playerResponseMethod: MutableMethod
+
+val playerResponseMethodHookPatch = bytecodePatch(
+ description = "playerResponseMethodHookPatch"
+) {
+ dependsOn(versionCheckPatch)
+
+ execute {
+ playerResponseMethod = if (is_7_03_or_greater) {
+ playerParameterBuilderFingerprint
+ } else {
+ playerParameterBuilderLegacyFingerprint
+ }.methodOrThrow()
+ }
+}
+
+fun hookPlayerResponse(
+ descriptor: String,
+ onlyVideoId: Boolean = false
+) {
+ val smaliInstruction = if (onlyVideoId)
+ "invoke-static {$REGISTER_VIDEO_ID}, $descriptor"
+ else
+ "invoke-static {$REGISTER_VIDEO_ID, $REGISTER_PLAYLIST_ID, $REGISTER_PLAYLIST_INDEX}, $descriptor"
+
+ playerResponseMethod.addInstruction(0, smaliInstruction)
+}
diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/ads/BaseAdsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/ads/BaseAdsPatch.kt
index 66863f474..02cf1f243 100644
--- a/patches/src/main/kotlin/app/revanced/patches/shared/ads/BaseAdsPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/shared/ads/BaseAdsPatch.kt
@@ -10,7 +10,6 @@ import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
import app.revanced.patcher.util.smali.ExternalLabel
import app.revanced.patches.shared.extension.Constants.PATCHES_PATH
-import app.revanced.util.fingerprint.matchOrThrow
import app.revanced.util.fingerprint.methodOrThrow
import app.revanced.util.getReference
import app.revanced.util.getWalkerMethod
@@ -33,20 +32,15 @@ fun baseAdsPatch(
) {
execute {
- setOf(
- sslGuardFingerprint,
- videoAdsFingerprint,
- ).forEach { fingerprint ->
- fingerprint.methodOrThrow().apply {
- addInstructionsWithLabels(
- 0, """
- invoke-static {}, $classDescriptor->$methodDescriptor()Z
- move-result v0
- if-nez v0, :show_ads
- return-void
- """, ExternalLabel("show_ads", getInstruction(0))
- )
- }
+ videoAdsFingerprint.methodOrThrow().apply {
+ addInstructionsWithLabels(
+ 0, """
+ invoke-static {}, $classDescriptor->$methodDescriptor()Z
+ move-result v0
+ if-nez v0, :show_ads
+ return-void
+ """, ExternalLabel("show_ads", getInstruction(0))
+ )
}
musicAdsFingerprint.methodOrThrow().apply {
@@ -67,21 +61,6 @@ fun baseAdsPatch(
)
}
- advertisingIdFingerprint.matchOrThrow().let {
- it.method.apply {
- val insertIndex = it.stringMatches!!.first().index
- val insertRegister = getInstruction(insertIndex).registerA
- addInstructionsWithLabels(
- insertIndex, """
- invoke-static {}, $classDescriptor->$methodDescriptor()Z
- move-result v$insertRegister
- if-nez v$insertRegister, :enable_id
- return-void
- """, ExternalLabel("enable_id", getInstruction(insertIndex))
- )
- }
- }
-
}
}
diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/ads/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/shared/ads/Fingerprints.kt
index 6853546b4..9ec138962 100644
--- a/patches/src/main/kotlin/app/revanced/patches/shared/ads/Fingerprints.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/shared/ads/Fingerprints.kt
@@ -4,24 +4,6 @@ import app.revanced.util.fingerprint.legacyFingerprint
import app.revanced.util.or
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
-import com.android.tools.smali.dexlib2.util.MethodUtil
-
-internal val advertisingIdFingerprint = legacyFingerprint(
- name = "advertisingIdFingerprint",
- returnType = "V",
- strings = listOf("a."),
- customFingerprint = { method, classDef ->
- MethodUtil.isConstructor(method) &&
- classDef.fields.find { it.type == "Ljava/util/Random;" } != null
- }
-)
-
-internal val sslGuardFingerprint = legacyFingerprint(
- name = "sslGuardFingerprint",
- returnType = "V",
- accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
- strings = listOf("Cannot initialize SslGuardSocketFactory will null"),
-)
internal val musicAdsFingerprint = legacyFingerprint(
name = "musicAdsFingerprint",
diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/gms/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/shared/gms/Fingerprints.kt
index 8734a722f..1cff11b8f 100644
--- a/patches/src/main/kotlin/app/revanced/patches/shared/gms/Fingerprints.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/shared/gms/Fingerprints.kt
@@ -5,11 +5,8 @@ import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstruction
import app.revanced.util.or
import com.android.tools.smali.dexlib2.AccessFlags
-import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.Method
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
-import com.android.tools.smali.dexlib2.iface.reference.StringReference
-import com.android.tools.smali.dexlib2.util.MethodUtil
const val GET_GMS_CORE_VENDOR_GROUP_ID_METHOD_NAME = "getGmsCoreVendorGroupId"
@@ -25,15 +22,6 @@ internal val castContextFetchFingerprint = legacyFingerprint(
strings = listOf("Error fetching CastContext.")
)
-internal val castDynamiteModuleFingerprint = legacyFingerprint(
- name = "castDynamiteModuleFingerprint",
- strings = listOf("com.google.android.gms.cast.framework.internal.CastDynamiteModuleImpl")
-)
-internal val castDynamiteModuleV2Fingerprint = legacyFingerprint(
- name = "castDynamiteModuleV2Fingerprint",
- strings = listOf("Failed to load module via V2: ")
-)
-
internal val googlePlayUtilityFingerprint = legacyFingerprint(
name = "castContextFetchFingerprint",
returnType = "I",
@@ -53,42 +41,9 @@ internal val serviceCheckFingerprint = legacyFingerprint(
strings = listOf("Google Play Services not available")
)
-internal val primesApiFingerprint = legacyFingerprint(
- name = "primesApiFingerprint",
- returnType = "V",
- strings = listOf("PrimesApiImpl.java"),
- customFingerprint = { method, _ ->
- MethodUtil.isConstructor(method)
- }
-)
-
-internal val primesBackgroundInitializationFingerprint = legacyFingerprint(
- name = "primesBackgroundInitializationFingerprint",
- opcodes = listOf(Opcode.NEW_INSTANCE),
- customFingerprint = { method, _ ->
- method.indexOfFirstInstruction {
- opcode == Opcode.CONST_STRING &&
- getReference()
- ?.string.toString()
- .startsWith("Primes init triggered from background in package:")
- } >= 0
- }
-)
-
-internal val primesLifecycleEventFingerprint = legacyFingerprint(
- name = "primesLifecycleEventFingerprint",
- accessFlags = AccessFlags.PRIVATE or AccessFlags.FINAL,
- returnType = "V",
- parameters = emptyList(),
- opcodes = listOf(Opcode.NEW_INSTANCE),
- customFingerprint = { method, _ ->
- method.indexOfFirstInstruction {
- opcode == Opcode.CONST_STRING &&
- getReference()
- ?.string.toString()
- .startsWith("Primes did not observe lifecycle events in the expected order.")
- } >= 0
- }
+internal val primeMethodFingerprint = legacyFingerprint(
+ name = "primeMethodFingerprint",
+ strings = listOf("com.google.android.GoogleCamera", "com.android.vending")
)
internal val certificateFingerprint = legacyFingerprint(
diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/gms/GmsCoreSupportPatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/gms/GmsCoreSupportPatch.kt
index f22bd9fb4..57d27416f 100644
--- a/patches/src/main/kotlin/app/revanced/patches/shared/gms/GmsCoreSupportPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/shared/gms/GmsCoreSupportPatch.kt
@@ -1,9 +1,9 @@
package app.revanced.patches.shared.gms
import app.revanced.patcher.Fingerprint
-import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
+import app.revanced.patcher.extensions.InstructionExtensions.instructions
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
import app.revanced.patcher.patch.BytecodePatchBuilder
import app.revanced.patcher.patch.BytecodePatchContext
@@ -25,15 +25,12 @@ import app.revanced.util.fingerprint.methodOrThrow
import app.revanced.util.fingerprint.mutableClassOrThrow
import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstruction
-import app.revanced.util.indexOfFirstInstructionOrThrow
-import app.revanced.util.indexOfFirstInstructionReversedOrThrow
import app.revanced.util.returnEarly
import app.revanced.util.valueOrThrow
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction21c
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.formats.Instruction21c
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
import com.android.tools.smali.dexlib2.iface.reference.StringReference
@@ -233,41 +230,18 @@ fun gmsCoreSupportPatch(
}
}
- fun transformPrimeMethod() {
- setOf(
- primesBackgroundInitializationFingerprint,
- primesLifecycleEventFingerprint
- ).forEach { fingerprint ->
- fingerprint.methodOrThrow().apply {
- val exceptionIndex = indexOfFirstInstructionReversedOrThrow {
- opcode == Opcode.NEW_INSTANCE &&
- (this as? ReferenceInstruction)?.reference?.toString() == "Ljava/lang/IllegalStateException;"
- }
- val index =
- indexOfFirstInstructionReversedOrThrow(exceptionIndex, Opcode.IF_EQZ)
- val register = getInstruction(index).registerA
- addInstruction(
- index,
- "const/4 v$register, 0x1"
- )
- }
- }
- primesApiFingerprint.mutableClassOrThrow().methods.filter { method ->
- method.name != "" &&
- method.returnType == "V"
- }.forEach { method ->
- method.apply {
- val index = if (MethodUtil.isConstructor(method))
- indexOfFirstInstructionOrThrow {
- opcode == Opcode.INVOKE_DIRECT &&
- getReference()?.name == ""
- } + 1
- else 0
- addInstruction(
- index,
- "return-void"
- )
+ fun transformPrimeMethod(packageName: String) {
+ primeMethodFingerprint.methodOrThrow().apply {
+ var register = 2
+
+ val index = instructions.indexOfFirst {
+ if (it.getReference()?.string != fromPackageName) return@indexOfFirst false
+
+ register = (it as OneRegisterInstruction).registerA
+ return@indexOfFirst true
}
+
+ replaceInstruction(index, "const-string v$register, \"$packageName\"")
}
}
@@ -293,14 +267,12 @@ fun gmsCoreSupportPatch(
// Return these methods early to prevent the app from crashing.
setOf(
castContextFetchFingerprint,
- castDynamiteModuleFingerprint,
- castDynamiteModuleV2Fingerprint,
googlePlayUtilityFingerprint,
serviceCheckFingerprint,
).forEach { it.methodOrThrow().returnEarly() }
// Specific method that needs to be patched.
- transformPrimeMethod()
+ transformPrimeMethod(packageName)
// Verify GmsCore is installed and whitelisted for power optimizations and background usage.
mainActivityOnCreateFingerprint.method.apply {
@@ -358,134 +330,262 @@ private object Constants {
* All permissions.
*/
val PERMISSIONS = setOf(
- // C2DM / GCM
"com.google.android.c2dm.permission.RECEIVE",
"com.google.android.c2dm.permission.SEND",
- "com.google.android.gtalkservice.permission.GTALK_SERVICE",
- "com.google.android.providers.gsf.permission.READ_GSERVICES",
-
- // GAuth
+ "com.google.android.gms.auth.api.phone.permission.SEND",
+ "com.google.android.gms.permission.AD_ID",
+ "com.google.android.gms.permission.AD_ID_NOTIFICATION",
+ "com.google.android.gms.permission.CAR_FUEL",
+ "com.google.android.gms.permission.CAR_INFORMATION",
+ "com.google.android.gms.permission.CAR_MILEAGE",
+ "com.google.android.gms.permission.CAR_SPEED",
+ "com.google.android.gms.permission.CAR_VENDOR_EXTENSION",
"com.google.android.googleapps.permission.GOOGLE_AUTH",
"com.google.android.googleapps.permission.GOOGLE_AUTH.cp",
"com.google.android.googleapps.permission.GOOGLE_AUTH.local",
"com.google.android.googleapps.permission.GOOGLE_AUTH.mail",
"com.google.android.googleapps.permission.GOOGLE_AUTH.writely",
-
- // Ad
- "com.google.android.gms.permission.AD_ID_NOTIFICATION",
- "com.google.android.gms.permission.AD_ID",
+ "com.google.android.gtalkservice.permission.GTALK_SERVICE",
+ "com.google.android.providers.gsf.permission.READ_GSERVICES",
)
/**
* All intent actions.
*/
val ACTIONS = setOf(
- // location
- "com.google.android.gms.location.places.ui.PICK_PLACE",
- "com.google.android.gms.location.places.GeoDataApi",
- "com.google.android.gms.location.places.PlacesApi",
- "com.google.android.gms.location.places.PlaceDetectionApi",
- "com.google.android.gms.wearable.MESSAGE_RECEIVED",
- "com.google.android.gms.checkin.BIND_TO_SERVICE",
-
- // C2DM / GCM
+ "com.google.android.c2dm.intent.RECEIVE",
"com.google.android.c2dm.intent.REGISTER",
"com.google.android.c2dm.intent.REGISTRATION",
"com.google.android.c2dm.intent.UNREGISTER",
- "com.google.android.c2dm.intent.RECEIVE",
- "com.google.iid.TOKEN_REQUEST",
+ "com.google.android.contextmanager.service.ContextManagerService.START",
"com.google.android.gcm.intent.SEND",
-
- // car
- "com.google.android.gms.car.service.START",
-
- // people
- "com.google.android.gms.people.service.START",
-
- // wearable
- "com.google.android.gms.wearable.BIND",
-
- // auth
- "com.google.android.gsf.login",
- "com.google.android.gsf.action.GET_GLS",
- "com.google.android.gms.common.account.CHOOSE_ACCOUNT",
- "com.google.android.gms.auth.login.LOGIN",
+ "com.google.android.gms.accounts.ACCOUNT_SERVICE",
+ "com.google.android.gms.accountsettings.ACCOUNT_PREFERENCES_SETTINGS",
+ "com.google.android.gms.accountsettings.action.BROWSE_SETTINGS",
+ "com.google.android.gms.accountsettings.action.VIEW_SETTINGS",
+ "com.google.android.gms.accountsettings.MY_ACCOUNT",
+ "com.google.android.gms.accountsettings.PRIVACY_SETTINGS",
+ "com.google.android.gms.accountsettings.SECURITY_SETTINGS",
+ "com.google.android.gms.ads.gservice.START",
+ "com.google.android.gms.ads.identifier.service.EVENT_ATTESTATION",
+ "com.google.android.gms.ads.service.CACHE",
+ "com.google.android.gms.ads.service.CONSENT_LOOKUP",
+ "com.google.android.gms.ads.service.HTTP",
+ "com.google.android.gms.analytics.service.START",
+ "com.google.android.gms.app.settings.GoogleSettingsLink",
+ "com.google.android.gms.appstate.service.START",
+ "com.google.android.gms.appusage.service.START",
+ "com.google.android.gms.asterism.service.START",
+ "com.google.android.gms.audiomodem.service.AudioModemService.START",
+ "com.google.android.gms.audit.service.START",
+ "com.google.android.gms.auth.account.authapi.START",
+ "com.google.android.gms.auth.account.authenticator.auto.service.START",
+ "com.google.android.gms.auth.account.authenticator.chromeos.START",
+ "com.google.android.gms.auth.account.authenticator.tv.service.START",
+ "com.google.android.gms.auth.account.data.service.START",
"com.google.android.gms.auth.api.credentials.PICKER",
"com.google.android.gms.auth.api.credentials.service.START",
- "com.google.android.gms.auth.service.START",
- "com.google.firebase.auth.api.gms.service.START",
- "com.google.android.gms.auth.be.appcert.AppCertService",
- "com.google.android.gms.credential.manager.service.firstparty.START",
- "com.google.android.gms.auth.GOOGLE_SIGN_IN",
- "com.google.android.gms.signin.service.START",
- "com.google.android.gms.auth.api.signin.service.START",
+ "com.google.android.gms.auth.api.identity.service.authorization.START",
+ "com.google.android.gms.auth.api.identity.service.credentialsaving.START",
"com.google.android.gms.auth.api.identity.service.signin.START",
- "com.google.android.gms.accountsettings.action.VIEW_SETTINGS",
-
- // fido
- "com.google.android.gms.fido.fido2.privileged.START",
-
- // gass
- "com.google.android.gms.gass.START",
-
- // games
- "com.google.android.gms.games.service.START",
- "com.google.android.gms.games.PLAY_GAMES_UPGRADE",
- "com.google.android.gms.games.internal.connect.service.START",
-
- // help
- "com.google.android.gms.googlehelp.service.GoogleHelpService.START",
- "com.google.android.gms.googlehelp.HELP",
- "com.google.android.gms.feedback.internal.IFeedbackService",
-
- // cast
+ "com.google.android.gms.auth.api.phone.service.InternalService.START",
+ "com.google.android.gms.auth.api.signin.service.START",
+ "com.google.android.gms.auth.be.appcert.AppCertService",
+ "com.google.android.gms.auth.blockstore.service.START",
+ "com.google.android.gms.auth.config.service.START",
+ "com.google.android.gms.auth.cryptauth.cryptauthservice.START",
+ "com.google.android.gms.auth.GOOGLE_SIGN_IN",
+ "com.google.android.gms.auth.login.LOGIN",
+ "com.google.android.gms.auth.proximity.devicesyncservice.START",
+ "com.google.android.gms.auth.proximity.securechannelservice.START",
+ "com.google.android.gms.auth.proximity.START",
+ "com.google.android.gms.auth.service.START",
+ "com.google.android.gms.backup.ACTION_BACKUP_SETTINGS",
+ "com.google.android.gms.backup.G1_BACKUP",
+ "com.google.android.gms.backup.G1_RESTORE",
+ "com.google.android.gms.backup.GMS_MODULE_RESTORE",
+ "com.google.android.gms.beacon.internal.IBleService.START",
+ "com.google.android.gms.car.service.START",
+ "com.google.android.gms.carrierauth.service.START",
"com.google.android.gms.cast.firstparty.START",
+ "com.google.android.gms.cast.remote_display.service.START",
"com.google.android.gms.cast.service.BIND_CAST_DEVICE_CONTROLLER_SERVICE",
-
- // fonts
- "com.google.android.gms.fonts",
-
- // phenotype
- "com.google.android.gms.phenotype.service.START",
-
- // location
- "com.google.android.gms.location.reporting.service.START",
-
- // misc
- "com.google.android.gms.gmscompliance.service.START",
- "com.google.android.gms.oss.licenses.service.START",
- "com.google.android.gms.tapandpay.service.BIND",
- "com.google.android.gms.measurement.START",
- "com.google.android.gms.languageprofile.service.START",
+ "com.google.android.gms.cast_mirroring.service.START",
+ "com.google.android.gms.checkin.BIND_TO_SERVICE",
+ "com.google.android.gms.chromesync.service.START",
"com.google.android.gms.clearcut.service.START",
- "com.google.android.gms.icing.LIGHTWEIGHT_INDEX_SERVICE",
- "com.google.android.gms.icing.INDEX_SERVICE",
- "com.google.android.gms.mdm.services.START",
-
- // potoken
- "com.google.android.gms.potokens.service.START",
-
- // droidguard, safetynet
+ "com.google.android.gms.common.account.CHOOSE_ACCOUNT",
+ "com.google.android.gms.common.download.START",
+ "com.google.android.gms.common.service.START",
+ "com.google.android.gms.common.telemetry.service.START",
+ "com.google.android.gms.config.START",
+ "com.google.android.gms.constellation.service.START",
+ "com.google.android.gms.credential.manager.service.firstparty.START",
+ "com.google.android.gms.deviceconnection.service.START",
+ "com.google.android.gms.drive.ApiService.RESET_AFTER_BOOT",
+ "com.google.android.gms.drive.ApiService.START",
+ "com.google.android.gms.drive.ApiService.STOP",
+ "com.google.android.gms.droidguard.service.INIT",
+ "com.google.android.gms.droidguard.service.PING",
"com.google.android.gms.droidguard.service.START",
+ "com.google.android.gms.enterprise.loader.service.START",
+ "com.google.android.gms.facs.cache.service.START",
+ "com.google.android.gms.facs.internal.service.START",
+ "com.google.android.gms.feedback.internal.IFeedbackService",
+ "com.google.android.gms.fido.credentialstore.internal_service.START",
+ "com.google.android.gms.fido.fido2.privileged.START",
+ "com.google.android.gms.fido.fido2.regular.START",
+ "com.google.android.gms.fido.fido2.zeroparty.START",
+ "com.google.android.gms.fido.sourcedevice.service.START",
+ "com.google.android.gms.fido.targetdevice.internal_service.START",
+ "com.google.android.gms.fido.u2f.privileged.START",
+ "com.google.android.gms.fido.u2f.thirdparty.START",
+ "com.google.android.gms.fido.u2f.zeroparty.START",
+ "com.google.android.gms.fitness.BleApi",
+ "com.google.android.gms.fitness.ConfigApi",
+ "com.google.android.gms.fitness.GoalsApi",
+ "com.google.android.gms.fitness.GoogleFitnessService.START",
+ "com.google.android.gms.fitness.HistoryApi",
+ "com.google.android.gms.fitness.InternalApi",
+ "com.google.android.gms.fitness.RecordingApi",
+ "com.google.android.gms.fitness.SensorsApi",
+ "com.google.android.gms.fitness.SessionsApi",
+ "com.google.android.gms.fonts.service.START",
+ "com.google.android.gms.freighter.service.START",
+ "com.google.android.gms.games.internal.connect.service.START",
+ "com.google.android.gms.games.PLAY_GAMES_UPGRADE",
+ "com.google.android.gms.games.service.START",
+ "com.google.android.gms.gass.START",
+ "com.google.android.gms.gmscompliance.service.START",
+ "com.google.android.gms.googlehelp.HELP",
+ "com.google.android.gms.googlehelp.service.GoogleHelpService.START",
+ "com.google.android.gms.growth.service.START",
+ "com.google.android.gms.herrevad.services.LightweightNetworkQualityAndroidService.START",
+ "com.google.android.gms.icing.INDEX_SERVICE",
+ "com.google.android.gms.icing.LIGHTWEIGHT_INDEX_SERVICE",
+ "com.google.android.gms.identity.service.BIND",
+ "com.google.android.gms.inappreach.service.START",
+ "com.google.android.gms.instantapps.START",
+ "com.google.android.gms.kids.service.START",
+ "com.google.android.gms.languageprofile.service.START",
+ "com.google.android.gms.learning.internal.dynamitesupport.START",
+ "com.google.android.gms.learning.intservice.START",
+ "com.google.android.gms.learning.predictor.START",
+ "com.google.android.gms.learning.trainer.START",
+ "com.google.android.gms.learning.training.background.START",
+ "com.google.android.gms.location.places.GeoDataApi",
+ "com.google.android.gms.location.places.PlaceDetectionApi",
+ "com.google.android.gms.location.places.PlacesApi",
+ "com.google.android.gms.location.reporting.service.START",
+ "com.google.android.gms.location.settings.LOCATION_HISTORY",
+ "com.google.android.gms.location.settings.LOCATION_REPORTING_SETTINGS",
+ "com.google.android.gms.locationsharing.api.START",
+ "com.google.android.gms.locationsharingreporter.service.START",
+ "com.google.android.gms.lockbox.service.START",
+ "com.google.android.gms.matchstick.lighter.service.START",
+ "com.google.android.gms.mdm.services.DeviceManagerApiService.START",
+ "com.google.android.gms.mdm.services.START",
+ "com.google.android.gms.mdns.service.START",
+ "com.google.android.gms.measurement.START",
+ "com.google.android.gms.nearby.bootstrap.service.NearbyBootstrapService.START",
+ "com.google.android.gms.nearby.connection.service.START",
+ "com.google.android.gms.nearby.fastpair.START",
+ "com.google.android.gms.nearby.messages.service.NearbyMessagesService.START",
+ "com.google.android.gms.nearby.sharing.service.NearbySharingService.START",
+ "com.google.android.gms.nearby.sharing.START_SERVICE",
+ "com.google.android.gms.notifications.service.START",
+ "com.google.android.gms.ocr.service.internal.START",
+ "com.google.android.gms.ocr.service.START",
+ "com.google.android.gms.oss.licenses.service.START",
+ "com.google.android.gms.payse.service.BIND",
+ "com.google.android.gms.people.contactssync.service.START",
+ "com.google.android.gms.people.service.START",
+ "com.google.android.gms.phenotype.service.START",
+ "com.google.android.gms.photos.autobackup.service.START",
+ "com.google.android.gms.playlog.service.START",
+ "com.google.android.gms.plus.service.default.INTENT",
+ "com.google.android.gms.plus.service.image.INTENT",
+ "com.google.android.gms.plus.service.internal.START",
+ "com.google.android.gms.plus.service.START",
+ "com.google.android.gms.potokens.service.START",
+ "com.google.android.gms.pseudonymous.service.START",
+ "com.google.android.gms.rcs.START",
+ "com.google.android.gms.reminders.service.START",
+ "com.google.android.gms.romanesco.MODULE_BACKUP_AGENT",
+ "com.google.android.gms.romanesco.service.START",
"com.google.android.gms.safetynet.service.START",
+ "com.google.android.gms.scheduler.ACTION_PROXY_SCHEDULE",
+ "com.google.android.gms.search.service.SEARCH_AUTH_START",
+ "com.google.android.gms.semanticlocation.service.START_ODLH",
+ "com.google.android.gms.sesame.service.BIND",
+ "com.google.android.gms.settings.EXPOSURE_NOTIFICATION_SETTINGS",
+ "com.google.android.gms.setup.auth.SecondDeviceAuth.START",
+ "com.google.android.gms.signin.service.START",
+ "com.google.android.gms.smartdevice.d2d.SourceDeviceService.START",
+ "com.google.android.gms.smartdevice.d2d.TargetDeviceService.START",
+ "com.google.android.gms.smartdevice.directtransfer.SourceDirectTransferService.START",
+ "com.google.android.gms.smartdevice.directtransfer.TargetDirectTransferService.START",
+ "com.google.android.gms.smartdevice.postsetup.PostSetupService.START",
+ "com.google.android.gms.smartdevice.setup.accounts.AccountsService.START",
+ "com.google.android.gms.smartdevice.wifi.START_WIFI_HELPER_SERVICE",
+ "com.google.android.gms.social.location.activity.service.START",
+ "com.google.android.gms.speech.service.START",
+ "com.google.android.gms.statementservice.EXECUTE",
+ "com.google.android.gms.stats.ACTION_UPLOAD_DROPBOX_ENTRIES",
+ "com.google.android.gms.tapandpay.service.BIND",
+ "com.google.android.gms.telephonyspam.service.START",
+ "com.google.android.gms.testsupport.service.START",
+ "com.google.android.gms.thunderbird.service.START",
+ "com.google.android.gms.trustagent.BridgeApi.START",
+ "com.google.android.gms.trustagent.StateApi.START",
+ "com.google.android.gms.trustagent.trustlet.trustletmanagerservice.BIND",
+ "com.google.android.gms.trustlet.bluetooth.service.BIND",
+ "com.google.android.gms.trustlet.connectionlessble.service.BIND",
+ "com.google.android.gms.trustlet.face.service.BIND",
+ "com.google.android.gms.trustlet.nfc.service.BIND",
+ "com.google.android.gms.trustlet.onbody.service.BIND",
+ "com.google.android.gms.trustlet.place.service.BIND",
+ "com.google.android.gms.trustlet.voiceunlock.service.BIND",
+ "com.google.android.gms.udc.service.START",
+ "com.google.android.gms.update.START_API_SERVICE",
+ "com.google.android.gms.update.START_SERVICE",
+ "com.google.android.gms.update.START_SINGLE_USER_API_SERVICE",
+ "com.google.android.gms.update.START_TV_API_SERVICE",
+ "com.google.android.gms.usagereporting.service.START",
+ "com.google.android.gms.userlocation.service.START",
+ "com.google.android.gms.vehicle.cabin.service.START",
+ "com.google.android.gms.vehicle.climate.service.START",
+ "com.google.android.gms.vehicle.info.service.START",
+ "com.google.android.gms.wallet.service.BIND",
+ "com.google.android.gms.walletp2p.service.firstparty.BIND",
+ "com.google.android.gms.walletp2p.service.zeroparty.BIND",
+ "com.google.android.gms.wearable.BIND",
+ "com.google.android.gms.wearable.BIND_LISTENER",
+ "com.google.android.gms.wearable.DATA_CHANGED",
+ "com.google.android.gms.wearable.MESSAGE_RECEIVED",
+ "com.google.android.gms.wearable.NODE_CHANGED",
+ "com.google.android.gsf.action.GET_GLS",
+ "com.google.android.location.settings.LOCATION_REPORTING_SETTINGS",
+ "com.google.android.mdd.service.START",
+ "com.google.android.mdh.service.listener.START",
+ "com.google.android.mdh.service.START",
+ "com.google.android.mobstore.service.START",
+ "com.google.firebase.auth.api.gms.service.START",
+ "com.google.firebase.dynamiclinks.service.START",
+ "com.google.iid.TOKEN_REQUEST",
+ "com.google.android.gms.location.places.ui.PICK_PLACE",
)
/**
* All content provider authorities.
*/
val AUTHORITIES = setOf(
- // gsf
+ "com.google.android.gms.auth.accounts",
+ "com.google.android.gms.chimera",
+ "com.google.android.gms.fonts",
+ "com.google.android.gms.phenotype",
"com.google.android.gsf.gservices",
"com.google.settings",
-
- // auth
- "com.google.android.gms.auth.accounts",
-
- // fonts
- "com.google.android.gms.fonts",
-
- // phenotype
- "com.google.android.gms.phenotype",
)
}
diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/mainactivity/BaseMainActivityResolvePatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/mainactivity/BaseMainActivityResolvePatch.kt
index 8122949da..af3c7355f 100644
--- a/patches/src/main/kotlin/app/revanced/patches/shared/mainactivity/BaseMainActivityResolvePatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/shared/mainactivity/BaseMainActivityResolvePatch.kt
@@ -18,6 +18,10 @@ lateinit var onConfigurationChangedMethod: MutableMethod
private set
lateinit var onCreateMethod: MutableMethod
private set
+lateinit var onStartMethod: MutableMethod
+ private set
+lateinit var onStopMethod: MutableMethod
+ private set
private lateinit var constructorMethod: MutableMethod
private lateinit var onBackPressedMethod: MutableMethod
@@ -45,6 +49,9 @@ fun baseMainActivityResolvePatch(
// set onConfigurationChanged method
onConfigurationChangedMethod = getMainActivityMethod("onConfigurationChanged")
+
+ onStartMethod = getMainActivityMethod("onStart")
+ onStopMethod = getMainActivityMethod("onStop")
}
}
diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/materialyou/BaseMaterialYouPatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/materialyou/BaseMaterialYouPatch.kt
new file mode 100644
index 000000000..ca07af5af
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/shared/materialyou/BaseMaterialYouPatch.kt
@@ -0,0 +1,119 @@
+package app.revanced.patches.shared.materialyou
+
+import app.revanced.patcher.patch.ResourcePatchContext
+import org.w3c.dom.Element
+import java.nio.file.Files
+
+private fun ResourcePatchContext.patchXmlFile(
+ fromDir: String,
+ toDir: String,
+ xmlFileName: String,
+ parentNode: String,
+ targetNode: String? = null,
+ attribute: String,
+ newValue: String
+) {
+ val resourceDirectory = get("res")
+ val fromDirectory = resourceDirectory.resolve(fromDir)
+ val toDirectory = resourceDirectory.resolve(toDir)
+
+ if (!toDirectory.isDirectory) Files.createDirectories(toDirectory.toPath())
+
+ val fromXmlFile = fromDirectory.resolve(xmlFileName)
+ val toXmlFile = toDirectory.resolve(xmlFileName)
+
+ if (!fromXmlFile.exists()) {
+ return
+ }
+
+ if (!toXmlFile.exists()) {
+ Files.copy(
+ fromXmlFile.toPath(),
+ toXmlFile.toPath()
+ )
+ }
+
+ document("res/$toDir/$xmlFileName").use { document ->
+ val parentList = document.getElementsByTagName(parentNode).item(0) as Element
+
+ if (targetNode != null) {
+ for (i in 0 until parentList.childNodes.length) {
+ val node = parentList.childNodes.item(i) as? Element ?: continue
+
+ if (node.nodeName == targetNode && node.hasAttribute(attribute)) {
+ node.getAttributeNode(attribute).textContent = newValue
+ }
+ }
+ } else {
+ if (parentList.hasAttribute(attribute)) {
+ parentList.getAttributeNode(attribute).textContent = newValue
+ }
+ }
+ }
+}
+
+fun ResourcePatchContext.baseMaterialYou() {
+ patchXmlFile(
+ "drawable",
+ "drawable-night-v31",
+ "new_content_dot_background.xml",
+ "shape",
+ "solid",
+ "android:color",
+ "@android:color/system_accent1_100"
+ )
+ patchXmlFile(
+ "drawable",
+ "drawable-night-v31",
+ "new_content_dot_background_cairo.xml",
+ "shape",
+ "solid",
+ "android:color",
+ "@android:color/system_accent1_100"
+ )
+ patchXmlFile(
+ "drawable",
+ "drawable-v31",
+ "new_content_dot_background.xml",
+ "shape",
+ "solid",
+ "android:color",
+ "@android:color/system_accent1_200"
+ )
+ patchXmlFile(
+ "drawable",
+ "drawable-v31",
+ "new_content_dot_background_cairo.xml",
+ "shape",
+ "solid",
+ "android:color",
+ "@android:color/system_accent1_200"
+ )
+ patchXmlFile(
+ "drawable",
+ "drawable-v31",
+ "new_content_count_background.xml",
+ "shape",
+ "solid",
+ "android:color",
+ "@android:color/system_accent1_100"
+ )
+ patchXmlFile(
+ "drawable",
+ "drawable-v31",
+ "new_content_count_background_cairo.xml",
+ "shape",
+ "solid",
+ "android:color",
+ "@android:color/system_accent1_100"
+ )
+ patchXmlFile(
+ "layout",
+ "layout-v31",
+ "new_content_count.xml",
+ "TextView",
+ null,
+ "android:textColor",
+ "@android:color/system_neutral1_900"
+ )
+}
\ No newline at end of file
diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/spoof/streamingdata/BaseSpoofStreamingDataPatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/spoof/streamingdata/BaseSpoofStreamingDataPatch.kt
index 0ddda9abf..6d9012f87 100644
--- a/patches/src/main/kotlin/app/revanced/patches/shared/spoof/streamingdata/BaseSpoofStreamingDataPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/shared/spoof/streamingdata/BaseSpoofStreamingDataPatch.kt
@@ -6,13 +6,11 @@ import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWith
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
import app.revanced.patcher.extensions.InstructionExtensions.instructions
import app.revanced.patcher.extensions.InstructionExtensions.removeInstruction
-import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
import app.revanced.patcher.patch.BytecodePatchBuilder
import app.revanced.patcher.patch.BytecodePatchContext
import app.revanced.patcher.patch.PatchException
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable
-import app.revanced.patches.shared.extension.Constants.PATCHES_PATH
import app.revanced.patches.shared.extension.Constants.SPOOF_PATH
import app.revanced.patches.shared.formatStreamModelConstructorFingerprint
import app.revanced.util.findInstructionIndicesReversedOrThrow
@@ -21,6 +19,7 @@ import app.revanced.util.fingerprint.definingClassOrThrow
import app.revanced.util.fingerprint.injectLiteralInstructionBooleanCall
import app.revanced.util.fingerprint.matchOrThrow
import app.revanced.util.fingerprint.methodOrThrow
+import app.revanced.util.fingerprint.mutableClassOrThrow
import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstructionOrThrow
import com.android.tools.smali.dexlib2.AccessFlags
@@ -117,15 +116,20 @@ fun baseSpoofStreamingDataPatch(
// region Replace the streaming data.
val approxDurationMsReference = formatStreamModelConstructorFingerprint.matchOrThrow().let {
- with (it.method) {
+ with(it.method) {
getInstruction(it.patternMatch!!.startIndex).reference
}
}
- val streamingDataFormatsReference = with(videoStreamingDataConstructorFingerprint.methodOrThrow(videoStreamingDataToStringFingerprint)) {
+ val streamingDataFormatsReference = with(
+ videoStreamingDataConstructorFingerprint.methodOrThrow(
+ videoStreamingDataToStringFingerprint
+ )
+ ) {
val getFormatsFieldIndex = indexOfGetFormatsFieldInstruction(this)
val longMaxValueIndex = indexOfLongMaxValueInstruction(this, getFormatsFieldIndex)
- val longMaxValueRegister = getInstruction(longMaxValueIndex).registerA
+ val longMaxValueRegister =
+ getInstruction(longMaxValueIndex).registerA
val videoIdIndex =
indexOfFirstInstructionOrThrow(longMaxValueIndex) {
val reference = getReference()
@@ -362,12 +366,23 @@ fun baseSpoofStreamingDataPatch(
// endregion
- findMethodOrThrow("$PATCHES_PATH/PatchStatus;") {
- name == "SpoofStreamingData"
- }.replaceInstruction(
- 0,
- "const/4 v0, 0x1"
- )
+ // region Set DroidGuard poToken.
+
+ poTokenToStringFingerprint.mutableClassOrThrow().let {
+ val poTokenClass = it.fields.find { field ->
+ field.accessFlags == AccessFlags.PRIVATE.value && field.type.startsWith("L")
+ }!!.type
+
+ findMethodOrThrow(poTokenClass) {
+ name == "" &&
+ parameters == listOf("[B")
+ }.addInstruction(
+ 1,
+ "invoke-static { p1 }, $EXTENSION_CLASS_DESCRIPTOR->setDroidGuardPoToken([B)V"
+ )
+ }
+
+ // endregion
executeBlock()
diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/spoof/streamingdata/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/shared/spoof/streamingdata/Fingerprints.kt
index 36ae9360b..265b1c245 100644
--- a/patches/src/main/kotlin/app/revanced/patches/shared/spoof/streamingdata/Fingerprints.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/shared/spoof/streamingdata/Fingerprints.kt
@@ -197,3 +197,22 @@ internal val hlsCurrentTimeFingerprint = legacyFingerprint(
parameters = listOf("Z", "L"),
literals = listOf(HLS_CURRENT_TIME_FEATURE_FLAG),
)
+
+internal val poTokenToStringFingerprint = legacyFingerprint(
+ name = "poTokenToStringFingerprint",
+ returnType = "Ljava/lang/String;",
+ accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
+ parameters = emptyList(),
+ strings = listOf("UTF-8"),
+ customFingerprint = { method, classDef ->
+ method.name == "toString" &&
+ classDef.fields.find { it.type == "[B" } != null &&
+ // In YouTube, this field's type is 'Lcom/google/android/gms/potokens/PoToken;'.
+ // In YouTube Music, this class name is obfuscated.
+ classDef.fields.find {
+ it.accessFlags == AccessFlags.PRIVATE.value && it.type.startsWith(
+ "L"
+ )
+ } != null
+ },
+)
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/general/components/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/general/components/Fingerprints.kt
index e37d96d1d..0acbf1fa6 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/general/components/Fingerprints.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/general/components/Fingerprints.kt
@@ -134,3 +134,12 @@ internal val tooltipContentViewFingerprint = legacyFingerprint(
literals = listOf(toolTipContentView),
)
+internal const val TRANSLUCENT_STATUS_BAR_FEATURE_FLAG = 45400535L
+
+internal val translucentStatusBarFeatureFlagFingerprint = legacyFingerprint(
+ name = "translucentStatusBarFeatureFlagFingerprint",
+ accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
+ returnType = "Z",
+ literals = listOf(TRANSLUCENT_STATUS_BAR_FEATURE_FLAG)
+)
+
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/general/components/LayoutComponentsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/general/components/LayoutComponentsPatch.kt
index a80c7b44b..05b29167a 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/general/components/LayoutComponentsPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/general/components/LayoutComponentsPatch.kt
@@ -17,10 +17,13 @@ import app.revanced.patches.youtube.utils.extension.Constants.COMPONENTS_PATH
import app.revanced.patches.youtube.utils.extension.Constants.GENERAL_CLASS_DESCRIPTOR
import app.revanced.patches.youtube.utils.extension.Constants.GENERAL_PATH
import app.revanced.patches.youtube.utils.patch.PatchList.HIDE_LAYOUT_COMPONENTS
+import app.revanced.patches.youtube.utils.playservice.is_19_25_or_greater
+import app.revanced.patches.youtube.utils.playservice.versionCheckPatch
import app.revanced.patches.youtube.utils.resourceid.accountSwitcherAccessibility
import app.revanced.patches.youtube.utils.resourceid.sharedResourceIdPatch
import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference
import app.revanced.patches.youtube.utils.settings.settingsPatch
+import app.revanced.util.fingerprint.injectLiteralInstructionBooleanCall
import app.revanced.util.fingerprint.matchOrThrow
import app.revanced.util.fingerprint.methodOrThrow
import app.revanced.util.fingerprint.mutableClassOrThrow
@@ -55,10 +58,16 @@ val layoutComponentsPatch = bytecodePatch(
sharedResourceIdPatch,
settingsMenuPatch,
viewGroupMarginLayoutParamsHookPatch,
+ versionCheckPatch,
)
execute {
+ var settingArray = arrayOf(
+ "PREFERENCE_SCREEN: GENERAL",
+ "SETTINGS: HIDE_LAYOUT_COMPONENTS"
+ )
+
// region patch for disable pip notification
pipNotificationFingerprint.matchOrThrow().let {
@@ -86,6 +95,19 @@ val layoutComponentsPatch = bytecodePatch(
// endregion
+ // region patch for disable translucent status bar
+
+ if (is_19_25_or_greater) {
+ translucentStatusBarFeatureFlagFingerprint.injectLiteralInstructionBooleanCall(
+ TRANSLUCENT_STATUS_BAR_FEATURE_FLAG,
+ "$GENERAL_CLASS_DESCRIPTOR->disableTranslucentStatusBar(Z)Z"
+ )
+
+ settingArray += "SETTINGS: DISABLE_TRANSLUCENT_STATUS_BAR"
+ }
+
+ // endregion
+
// region patch for disable update screen
appBlockingCheckResultToStringFingerprint.mutableClassOrThrow().methods.first { method ->
@@ -234,10 +256,7 @@ val layoutComponentsPatch = bytecodePatch(
// region add settings
addPreference(
- arrayOf(
- "PREFERENCE_SCREEN: GENERAL",
- "SETTINGS: HIDE_LAYOUT_COMPONENTS"
- ),
+ settingArray,
HIDE_LAYOUT_COMPONENTS
)
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/general/navigation/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/general/navigation/Fingerprints.kt
index fd74a28bd..33821a219 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/general/navigation/Fingerprints.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/general/navigation/Fingerprints.kt
@@ -74,32 +74,9 @@ internal val setEnumMapFingerprint = legacyFingerprint(
literals = listOf(ytFillBell),
)
-internal const val TRANSLUCENT_NAVIGATION_STATUS_BAR_FEATURE_FLAG = 45400535L
+internal const val TRANSLUCENT_NAVIGATION_BAR_FEATURE_FLAG = 45630927L
-internal val translucentNavigationStatusBarFeatureFlagFingerprint = legacyFingerprint(
- name = "translucentNavigationStatusBarFeatureFlagFingerprint",
- accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
- returnType = "Z",
- literals = listOf(TRANSLUCENT_NAVIGATION_STATUS_BAR_FEATURE_FLAG)
-)
-
-internal const val TRANSLUCENT_NAVIGATION_BUTTONS_FEATURE_FLAG = 45630927L
-
-internal val translucentNavigationButtonsFeatureFlagFingerprint = legacyFingerprint(
- name = "translucentNavigationButtonsFeatureFlagFingerprint",
- accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
- returnType = "V",
- literals = listOf(TRANSLUCENT_NAVIGATION_BUTTONS_FEATURE_FLAG)
-)
-
-/**
- * The device on screen back/home/recent buttons.
- */
-internal const val TRANSLUCENT_NAVIGATION_BUTTONS_SYSTEM_FEATURE_FLAG = 45632194L
-
-internal val translucentNavigationButtonsSystemFeatureFlagFingerprint = legacyFingerprint(
- name = "translucentNavigationButtonsSystemFeatureFlagFingerprint",
- accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
- returnType = "Z",
- literals = listOf(TRANSLUCENT_NAVIGATION_BUTTONS_SYSTEM_FEATURE_FLAG)
+internal val translucentNavigationBarFingerprint = legacyFingerprint(
+ name = "translucentNavigationBarFingerprint",
+ literals = listOf(TRANSLUCENT_NAVIGATION_BAR_FEATURE_FLAG),
)
\ No newline at end of file
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/general/navigation/NavigationBarComponentsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/general/navigation/NavigationBarComponentsPatch.kt
index 8e8d38987..ee584d225 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/general/navigation/NavigationBarComponentsPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/general/navigation/NavigationBarComponentsPatch.kt
@@ -85,30 +85,11 @@ val navigationBarComponentsPatch = bytecodePatch(
// region patch for enable translucent navigation bar
if (is_19_25_or_greater) {
- arrayOf(
- Triple(
- translucentNavigationStatusBarFeatureFlagFingerprint,
- TRANSLUCENT_NAVIGATION_STATUS_BAR_FEATURE_FLAG,
- "useTranslucentNavigationStatusBar"
- ),
- Triple(
- translucentNavigationButtonsFeatureFlagFingerprint,
- TRANSLUCENT_NAVIGATION_BUTTONS_FEATURE_FLAG,
- "useTranslucentNavigationButtons"
- ),
- Triple(
- translucentNavigationButtonsSystemFeatureFlagFingerprint,
- TRANSLUCENT_NAVIGATION_BUTTONS_SYSTEM_FEATURE_FLAG,
- "useTranslucentNavigationButtons"
- )
- ).forEach {
- it.first.injectLiteralInstructionBooleanCall(
- it.second,
- "$GENERAL_CLASS_DESCRIPTOR->${it.third}(Z)Z"
- )
- }
+ translucentNavigationBarFingerprint.injectLiteralInstructionBooleanCall(
+ TRANSLUCENT_NAVIGATION_BAR_FEATURE_FLAG,
+ "$GENERAL_CLASS_DESCRIPTOR->enableTranslucentNavigationBar()Z"
+ )
- settingArray += "SETTINGS: DISABLE_TRANSLUCENT_STATUS_BAR"
settingArray += "SETTINGS: TRANSLUCENT_NAVIGATION_BAR"
}
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/general/toolbar/ToolBarComponentsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/general/toolbar/ToolBarComponentsPatch.kt
index 74f762141..46233d174 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/general/toolbar/ToolBarComponentsPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/general/toolbar/ToolBarComponentsPatch.kt
@@ -342,10 +342,11 @@ val toolBarComponentsPatch = bytecodePatch(
opcode == Opcode.INVOKE_VIRTUAL &&
getReference()?.toString() == voiceInputControllerActivityMethodCall
}
- val setOnClickListenerIndex = indexOfFirstInstructionOrThrow(voiceInputControllerActivityIndex) {
- opcode == Opcode.INVOKE_VIRTUAL &&
- getReference()?.name == "setOnClickListener"
- }
+ val setOnClickListenerIndex =
+ indexOfFirstInstructionOrThrow(voiceInputControllerActivityIndex) {
+ opcode == Opcode.INVOKE_VIRTUAL &&
+ getReference()?.name == "setOnClickListener"
+ }
val viewRegister =
getInstruction(setOnClickListenerIndex).registerC
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/branding/icon/CustomBrandingIconPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/branding/icon/CustomBrandingIconPatch.kt
index ededd6ab4..797477de2 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/branding/icon/CustomBrandingIconPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/branding/icon/CustomBrandingIconPatch.kt
@@ -5,12 +5,14 @@ import app.revanced.patcher.patch.resourcePatch
import app.revanced.patcher.patch.stringOption
import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE
import app.revanced.patches.youtube.utils.patch.PatchList.CUSTOM_BRANDING_ICON_FOR_YOUTUBE
+import app.revanced.patches.youtube.utils.playservice.is_19_17_or_greater
import app.revanced.patches.youtube.utils.playservice.is_19_32_or_greater
import app.revanced.patches.youtube.utils.playservice.is_19_34_or_greater
import app.revanced.patches.youtube.utils.playservice.versionCheckPatch
import app.revanced.patches.youtube.utils.settings.ResourceUtils.updatePatchStatusIcon
import app.revanced.patches.youtube.utils.settings.settingsPatch
import app.revanced.util.ResourceGroup
+import app.revanced.util.Utils.printWarn
import app.revanced.util.Utils.trimIndentMultiline
import app.revanced.util.copyAdaptiveIcon
import app.revanced.util.copyFile
@@ -122,7 +124,7 @@ val customBrandingIconPatch = resourcePatch(
key = "changeSplashIcon",
default = true,
title = "Change splash icons",
- description = "Apply the custom branding icon to the splash screen.",
+ description = "Apply the custom branding icon to the splash screen. Supports from YouTube 18.29.38 to YouTube 19.16.39.",
required = true
)
@@ -171,29 +173,33 @@ val customBrandingIconPatch = resourcePatch(
// Change splash icon.
if (changeSplashIconOption == true) {
- splashIconResourceGroups.let { resourceGroups ->
- resourceGroups.forEach {
- copyResources("$appIconResourcePath/splash", it)
- }
- }
-
- document("res/values/styles.xml").use { document ->
- val resourcesNode =
- document.getElementsByTagName("resources").item(0) as Element
- val childNodes = resourcesNode.childNodes
-
- for (i in 0 until childNodes.length) {
- val node = childNodes.item(i) as? Element ?: continue
- val nodeAttributeName = node.getAttribute("name")
- if (nodeAttributeName.startsWith("Theme.YouTube.Launcher")) {
- val style = document.createElement("style")
- style.setAttribute("name", nodeAttributeName)
- style.setAttribute("parent", "@style/Base.Theme.YouTube.Launcher")
-
- resourcesNode.removeChild(node)
- resourcesNode.appendChild(style)
+ if (!is_19_17_or_greater) {
+ splashIconResourceGroups.let { resourceGroups ->
+ resourceGroups.forEach {
+ copyResources("$appIconResourcePath/splash", it)
}
}
+
+ document("res/values/styles.xml").use { document ->
+ val resourcesNode =
+ document.getElementsByTagName("resources").item(0) as Element
+ val childNodes = resourcesNode.childNodes
+
+ for (i in 0 until childNodes.length) {
+ val node = childNodes.item(i) as? Element ?: continue
+ val nodeAttributeName = node.getAttribute("name")
+ if (nodeAttributeName.startsWith("Theme.YouTube.Launcher")) {
+ val style = document.createElement("style")
+ style.setAttribute("name", nodeAttributeName)
+ style.setAttribute("parent", "@style/Base.Theme.YouTube.Launcher")
+
+ resourcesNode.removeChild(node)
+ resourcesNode.appendChild(style)
+ }
+ }
+ }
+ } else {
+ printWarn("\"Change splash icons\" is not supported in this version. Use YouTube 19.16.39 or earlier.")
}
}
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/theme/MaterialYouPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/theme/MaterialYouPatch.kt
index 9688864a0..c148fcb4d 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/theme/MaterialYouPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/theme/MaterialYouPatch.kt
@@ -1,14 +1,13 @@
package app.revanced.patches.youtube.layout.theme
import app.revanced.patcher.patch.resourcePatch
+import app.revanced.patches.shared.materialyou.baseMaterialYou
import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE
import app.revanced.patches.youtube.utils.patch.PatchList.MATERIALYOU
import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference
import app.revanced.patches.youtube.utils.settings.ResourceUtils.updatePatchStatusTheme
import app.revanced.patches.youtube.utils.settings.settingsPatch
import app.revanced.util.copyXmlNode
-import org.w3c.dom.Element
-import java.nio.file.Files
@Suppress("unused")
val materialYouPatch = resourcePatch(
@@ -24,117 +23,7 @@ val materialYouPatch = resourcePatch(
)
execute {
- fun patchXmlFile(
- fromDir: String,
- toDir: String,
- xmlFileName: String,
- parentNode: String,
- targetNode: String? = null,
- attribute: String,
- newValue: String
- ) {
- val resourceDirectory = get("res")
- val fromDirectory = resourceDirectory.resolve(fromDir)
- val toDirectory = resourceDirectory.resolve(toDir)
-
- if (!toDirectory.isDirectory) Files.createDirectories(toDirectory.toPath())
-
- val fromXmlFile = fromDirectory.resolve(xmlFileName)
- val toXmlFile = toDirectory.resolve(xmlFileName)
-
- if (!fromXmlFile.exists()) {
- return
- }
-
- if (!toXmlFile.exists()) {
- Files.copy(
- fromXmlFile.toPath(),
- toXmlFile.toPath()
- )
- }
-
- document("res/$toDir/$xmlFileName").use { document ->
- val parentList = document.getElementsByTagName(parentNode).item(0) as Element
-
- if (targetNode != null) {
- for (i in 0 until parentList.childNodes.length) {
- val node = parentList.childNodes.item(i) as? Element ?: continue
-
- if (node.nodeName == targetNode && node.hasAttribute(attribute)) {
- node.getAttributeNode(attribute).textContent = newValue
- }
- }
- } else {
- if (parentList.hasAttribute(attribute)) {
- parentList.getAttributeNode(attribute).textContent = newValue
- }
- }
- }
- }
-
- patchXmlFile(
- "drawable",
- "drawable-night-v31",
- "new_content_dot_background.xml",
- "shape",
- "solid",
- "android:color",
- "@android:color/system_accent1_100"
- )
- patchXmlFile(
- "drawable",
- "drawable-night-v31",
- "new_content_dot_background_cairo.xml",
- "shape",
- "solid",
- "android:color",
- "@android:color/system_accent1_100"
- )
- patchXmlFile(
- "drawable",
- "drawable-v31",
- "new_content_dot_background.xml",
- "shape",
- "solid",
- "android:color",
- "@android:color/system_accent1_200"
- )
- patchXmlFile(
- "drawable",
- "drawable-v31",
- "new_content_dot_background_cairo.xml",
- "shape",
- "solid",
- "android:color",
- "@android:color/system_accent1_200"
- )
- patchXmlFile(
- "drawable",
- "drawable-v31",
- "new_content_count_background.xml",
- "shape",
- "solid",
- "android:color",
- "@android:color/system_accent1_100"
- )
- patchXmlFile(
- "drawable",
- "drawable-v31",
- "new_content_count_background_cairo.xml",
- "shape",
- "solid",
- "android:color",
- "@android:color/system_accent1_100"
- )
- patchXmlFile(
- "layout",
- "layout-v31",
- "new_content_count.xml",
- "TextView",
- null,
- "android:textColor",
- "@android:color/system_neutral1_900"
- )
+ baseMaterialYou()
copyXmlNode("youtube/materialyou/host", "values-v31/colors.xml", "resources")
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/player/fullscreen/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/player/fullscreen/Fingerprints.kt
index d89496da6..f65a4b917 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/player/fullscreen/Fingerprints.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/player/fullscreen/Fingerprints.kt
@@ -23,19 +23,6 @@ internal val broadcastReceiverFingerprint = legacyFingerprint(
}
)
-internal val clientSettingEndpointFingerprint = legacyFingerprint(
- name = "clientSettingEndpointFingerprint",
- returnType = "V",
- accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
- parameters = listOf("L", "Ljava/util/Map;"),
- strings = listOf(
- "OVERRIDE_EXIT_FULLSCREEN_TO_MAXIMIZED",
- "force_fullscreen",
- "start_watch_minimized",
- "watch"
- )
-)
-
internal val engagementPanelFingerprint = legacyFingerprint(
name = "engagementPanelFingerprint",
returnType = "L",
@@ -72,10 +59,3 @@ internal val relatedEndScreenResultsFingerprint = legacyFingerprint(
literals = listOf(appRelatedEndScreenResults),
)
-internal val videoPortraitParentFingerprint = legacyFingerprint(
- name = "videoPortraitParentFingerprint",
- returnType = "V",
- accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
- parameters = listOf("L", "Ljava/util/Map;"),
- strings = listOf("Acquiring NetLatencyActionLogger failed. taskId=")
-)
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/player/fullscreen/FullscreenComponentsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/player/fullscreen/FullscreenComponentsPatch.kt
index f00ad80bd..21f315edc 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/player/fullscreen/FullscreenComponentsPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/player/fullscreen/FullscreenComponentsPatch.kt
@@ -11,13 +11,18 @@ import app.revanced.patcher.util.smali.ExternalLabel
import app.revanced.patches.shared.litho.addLithoFilter
import app.revanced.patches.shared.litho.lithoFilterPatch
import app.revanced.patches.shared.mainactivity.onConfigurationChangedMethod
+import app.revanced.patches.shared.mainactivity.onStartMethod
+import app.revanced.patches.shared.mainactivity.onStopMethod
import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE
import app.revanced.patches.youtube.utils.extension.Constants.COMPONENTS_PATH
import app.revanced.patches.youtube.utils.extension.Constants.PATCH_STATUS_CLASS_DESCRIPTOR
import app.revanced.patches.youtube.utils.extension.Constants.PLAYER_CLASS_DESCRIPTOR
+import app.revanced.patches.youtube.utils.extension.Constants.PLAYER_PATH
+import app.revanced.patches.youtube.utils.fullscreen.fullscreenButtonHookPatch
import app.revanced.patches.youtube.utils.layoutConstructorFingerprint
import app.revanced.patches.youtube.utils.mainactivity.mainActivityResolvePatch
import app.revanced.patches.youtube.utils.patch.PatchList.FULLSCREEN_COMPONENTS
+import app.revanced.patches.youtube.utils.playertype.playerTypeHookPatch
import app.revanced.patches.youtube.utils.playservice.is_18_42_or_greater
import app.revanced.patches.youtube.utils.playservice.is_19_41_or_greater
import app.revanced.patches.youtube.utils.playservice.versionCheckPatch
@@ -28,7 +33,11 @@ import app.revanced.patches.youtube.utils.resourceid.sharedResourceIdPatch
import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference
import app.revanced.patches.youtube.utils.settings.settingsPatch
import app.revanced.patches.youtube.utils.youtubeControlsOverlayFingerprint
+import app.revanced.patches.youtube.video.information.hookBackgroundPlayVideoInformation
+import app.revanced.patches.youtube.video.information.videoEndMethod
+import app.revanced.patches.youtube.video.information.videoInformationPatch
import app.revanced.util.Utils.printWarn
+import app.revanced.util.addInstructionsAtControlFlowLabel
import app.revanced.util.findMethodOrThrow
import app.revanced.util.fingerprint.methodOrThrow
import app.revanced.util.fingerprint.mutableClassOrThrow
@@ -42,13 +51,18 @@ import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
-import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction
import com.android.tools.smali.dexlib2.iface.instruction.WideLiteralInstruction
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
private const val FILTER_CLASS_DESCRIPTOR =
"$COMPONENTS_PATH/QuickActionFilter;"
+private const val EXTENSION_ENTER_FULLSCREEN_CLASS_DESCRIPTOR =
+ "$PLAYER_PATH/EnterFullscreenPatch;"
+
+private const val EXTENSION_EXIT_FULLSCREEN_CLASS_DESCRIPTOR =
+ "$PLAYER_PATH/ExitFullscreenPatch;"
+
@Suppress("unused")
val fullscreenComponentsPatch = bytecodePatch(
FULLSCREEN_COMPONENTS.title,
@@ -58,8 +72,11 @@ val fullscreenComponentsPatch = bytecodePatch(
dependsOn(
settingsPatch,
+ playerTypeHookPatch,
lithoFilterPatch,
mainActivityResolvePatch,
+ fullscreenButtonHookPatch,
+ videoInformationPatch,
sharedResourceIdPatch,
versionCheckPatch,
)
@@ -107,6 +124,33 @@ val fullscreenComponentsPatch = bytecodePatch(
// endregion
+ // region patch for enter fullscreen
+
+ mapOf(
+ onStartMethod to "onAppForegrounded",
+ onStopMethod to "onAppBackgrounded"
+ ).forEach { (method, name) ->
+ method.addInstruction(
+ 0,
+ "invoke-static {}, $EXTENSION_ENTER_FULLSCREEN_CLASS_DESCRIPTOR->$name()V"
+ )
+ }
+
+ hookBackgroundPlayVideoInformation("$EXTENSION_ENTER_FULLSCREEN_CLASS_DESCRIPTOR->enterFullscreen(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;JZ)V")
+
+ // endregion
+
+ // region patch for exit fullscreen
+
+ videoEndMethod.apply {
+ addInstructionsAtControlFlowLabel(
+ implementation!!.instructions.lastIndex,
+ "invoke-static {}, $EXTENSION_EXIT_FULLSCREEN_CLASS_DESCRIPTOR->endOfVideoReached()V",
+ )
+ }
+
+ // endregion
+
// region patch for hide autoplay preview
layoutConstructorFingerprint.methodOrThrow().apply {
@@ -198,60 +242,6 @@ val fullscreenComponentsPatch = bytecodePatch(
// endregion
- // region patch for force fullscreen
-
- clientSettingEndpointFingerprint.methodOrThrow().apply {
- val getActivityIndex = indexOfFirstStringInstructionOrThrow("watch") + 2
- val getActivityReference =
- getInstruction(getActivityIndex).reference
- val classRegister =
- getInstruction(getActivityIndex).registerB
-
- val watchDescriptorMethodIndex =
- indexOfFirstStringInstructionOrThrow("start_watch_minimized") - 1
- val watchDescriptorRegister =
- getInstruction(watchDescriptorMethodIndex).registerD
-
- addInstructions(
- watchDescriptorMethodIndex, """
- invoke-static {v$watchDescriptorRegister}, $PLAYER_CLASS_DESCRIPTOR->forceFullscreen(Z)Z
- move-result v$watchDescriptorRegister
- """
- )
-
- // hooks Activity.
- val insertIndex = indexOfFirstStringInstructionOrThrow("force_fullscreen")
- val freeRegister = getInstruction(insertIndex).registerA
-
- addInstructions(
- insertIndex, """
- iget-object v$freeRegister, v$classRegister, $getActivityReference
- check-cast v$freeRegister, Landroid/app/Activity;
- invoke-static {v$freeRegister}, $PLAYER_CLASS_DESCRIPTOR->setWatchDescriptorActivity(Landroid/app/Activity;)V
- """
- )
- }
-
- videoPortraitParentFingerprint.methodOrThrow().apply {
- val stringIndex =
- indexOfFirstStringInstructionOrThrow("Acquiring NetLatencyActionLogger failed. taskId=")
- val invokeIndex =
- indexOfFirstInstructionOrThrow(stringIndex, Opcode.INVOKE_INTERFACE)
- val targetIndex = indexOfFirstInstructionOrThrow(invokeIndex, Opcode.CHECK_CAST)
- val targetClass =
- getInstruction(targetIndex).reference.toString()
-
- // add an instruction to check the vertical video
- findMethodOrThrow(targetClass) {
- parameters == listOf("I", "I", "Z")
- }.addInstruction(
- 1,
- "invoke-static {p1, p2}, $PLAYER_CLASS_DESCRIPTOR->setVideoPortrait(II)V"
- )
- }
-
- // endregion
-
// region patch for disable landscape mode
onConfigurationChangedMethod.apply {
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/player/hapticfeedback/hapticFeedbackPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/player/hapticfeedback/hapticFeedbackPatch.kt
index 22888a480..373619800 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/player/hapticfeedback/hapticFeedbackPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/player/hapticfeedback/hapticFeedbackPatch.kt
@@ -10,10 +10,12 @@ import app.revanced.patches.youtube.utils.extension.Constants.PLAYER_CLASS_DESCR
import app.revanced.patches.youtube.utils.patch.PatchList.DISABLE_HAPTIC_FEEDBACK
import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference
import app.revanced.patches.youtube.utils.settings.settingsPatch
-import app.revanced.util.fingerprint.methodOrThrow
-import app.revanced.util.indexOfFirstInstructionOrThrow
+import app.revanced.util.fingerprint.matchOrThrow
+import app.revanced.util.getReference
+import app.revanced.util.indexOfFirstInstructionReversedOrThrow
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
+import com.android.tools.smali.dexlib2.iface.reference.FieldReference
@Suppress("unused")
val hapticFeedbackPatch = bytecodePatch(
@@ -26,23 +28,29 @@ val hapticFeedbackPatch = bytecodePatch(
execute {
fun Pair.hookHapticFeedback(methodName: String) =
- methodOrThrow().apply {
- var index = 0
- var register = 0
+ matchOrThrow().let {
+ it.method.apply {
+ var index = 0
+ var register = 0
- if (name == "run") {
- index = indexOfFirstInstructionOrThrow(Opcode.SGET)
- register = getInstruction(index).registerA
- }
+ if (name == "run") {
+ val stringIndex = it.stringMatches!!.first().index
+ index = indexOfFirstInstructionReversedOrThrow(stringIndex) {
+ opcode == Opcode.SGET &&
+ getReference()?.toString() == "Landroid/os/Build${'$'}VERSION;->SDK_INT:I"
+ }
+ register = getInstruction(index).registerA
+ }
- addInstructionsWithLabels(
- index, """
+ addInstructionsWithLabels(
+ index, """
invoke-static {}, $PLAYER_CLASS_DESCRIPTOR->$methodName()Z
move-result v$register
if-eqz v$register, :vibrate
return-void
""", ExternalLabel("vibrate", getInstruction(index))
- )
+ )
+ }
}
arrayOf(
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/player/overlaybuttons/OverlayButtonsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/player/overlaybuttons/OverlayButtonsPatch.kt
index 5c023b1eb..3c07be84b 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/player/overlaybuttons/OverlayButtonsPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/player/overlaybuttons/OverlayButtonsPatch.kt
@@ -15,12 +15,15 @@ import app.revanced.patches.youtube.utils.patch.PatchList.OVERLAY_BUTTONS
import app.revanced.patches.youtube.utils.pip.pipStateHookPatch
import app.revanced.patches.youtube.utils.playercontrols.hookBottomControlButton
import app.revanced.patches.youtube.utils.playercontrols.playerControlsPatch
+import app.revanced.patches.youtube.utils.playservice.is_19_17_or_greater
+import app.revanced.patches.youtube.utils.playservice.versionCheckPatch
import app.revanced.patches.youtube.utils.resourceid.sharedResourceIdPatch
import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference
import app.revanced.patches.youtube.utils.settings.settingsPatch
import app.revanced.patches.youtube.video.information.videoEndMethod
import app.revanced.patches.youtube.video.information.videoInformationPatch
import app.revanced.util.ResourceGroup
+import app.revanced.util.Utils.printWarn
import app.revanced.util.copyResources
import app.revanced.util.copyXmlNode
import app.revanced.util.doRecursively
@@ -75,6 +78,7 @@ val overlayButtonsPatch = resourcePatch(
playerControlsPatch,
sharedResourceIdPatch,
settingsPatch,
+ versionCheckPatch,
)
val iconTypeOption = stringOption(
@@ -99,7 +103,7 @@ val overlayButtonsPatch = resourcePatch(
"Wider" to MARGIN_WIDER,
),
title = "Bottom margin",
- description = "The bottom margin for the overlay buttons and timestamp.",
+ description = "The bottom margin for the overlay buttons and timestamp. Supports from YouTube 18.29.38 to YouTube 19.16.39.",
required = true
)
@@ -107,7 +111,7 @@ val overlayButtonsPatch = resourcePatch(
key = "widerButtonsSpace",
default = false,
title = "Wider between-buttons space",
- description = "Prevent adjacent button presses by increasing the horizontal spacing between buttons.",
+ description = "Prevent adjacent button presses by increasing the horizontal spacing between buttons. Supports from YouTube 18.29.38 to YouTube 19.16.39.",
required = true
)
@@ -125,9 +129,19 @@ val overlayButtonsPatch = resourcePatch(
val iconType = iconTypeOption
.lowerCaseOrThrow()
- val marginBottom = bottomMarginOption
+ var marginBottom = bottomMarginOption
.lowerCaseOrThrow()
+ if (marginBottom != MARGIN_DEFAULT && is_19_17_or_greater) {
+ printWarn("\"Bottom margin\" is not supported in this version. Use YouTube 19.16.39 or earlier.")
+ marginBottom = MARGIN_DEFAULT
+ }
+
+ if (widerButtonsSpace == true && is_19_17_or_greater) {
+ printWarn("\"Wider between-buttons space\" is not supported in this version. Use YouTube 19.16.39 or earlier.")
+ }
+ val useWiderButtonsSpace = widerButtonsSpace == true && !is_19_17_or_greater
+
// Inject hooks for overlay buttons.
setOf(
"AlwaysRepeat;",
@@ -201,59 +215,76 @@ val overlayButtonsPatch = resourcePatch(
"android.support.constraint.ConstraintLayout"
)
- // Note: Do not modify fullscreen button and multiview button
- document("res/layout/youtube_controls_bottom_ui_container.xml").use { document ->
- document.doRecursively loop@{ node ->
- if (node !is Element) return@loop
+ var xmlFiles = arrayOf(
+ "youtube_controls_bottom_ui_container.xml"
+ )
+ if (!is_19_17_or_greater) {
+ xmlFiles += "youtube_controls_fullscreen_button.xml"
+ xmlFiles += "youtube_controls_cf_fullscreen_button.xml"
+ }
- // Change the relationship between buttons
- node.getAttributeNode("yt:layout_constraintRight_toLeftOf")
- ?.let { attribute ->
- if (attribute.textContent == "@id/fullscreen_button") {
- attribute.textContent = "@+id/speed_dialog_button"
+ xmlFiles.forEach { xmlFile ->
+ val targetXml = get("res").resolve("layout").resolve(xmlFile)
+ if (targetXml.exists()) {
+ document("res/layout/$xmlFile").use { document ->
+ document.doRecursively loop@{ node ->
+ if (node !is Element) return@loop
+
+ // Change the relationship between buttons
+ node.getAttributeNode("yt:layout_constraintRight_toLeftOf")
+ ?.let { attribute ->
+ if (attribute.textContent == "@id/fullscreen_button") {
+ attribute.textContent = "@+id/speed_dialog_button"
+ }
+ }
+
+ val (id, height, width) = Triple(
+ node.getAttribute("android:id"),
+ node.getAttribute("android:layout_height"),
+ node.getAttribute("android:layout_width")
+ )
+ val (heightIsNotZero, widthIsNotZero) = Pair(
+ height != "0.0dip",
+ width != "0.0dip",
+ )
+
+ val isButton = if (is_19_17_or_greater)
+ // Note: Do not modify fullscreen button and multiview button
+ id.endsWith("_button") && id != "@id/multiview_button"
+ else
+ id.endsWith("_button") || id == "@id/youtube_controls_fullscreen_button_stub"
+
+ // Adjust TimeBar and Chapter bottom padding
+ val timBarItem = mutableMapOf(
+ "@id/time_bar_chapter_title" to "16.0dip",
+ "@id/timestamps_container" to "14.0dip"
+ )
+
+ val layoutHeightWidth = if (useWiderButtonsSpace)
+ "56.0dip"
+ else
+ "48.0dip"
+
+ if (isButton) {
+ node.setAttribute("android:layout_marginBottom", marginBottom)
+ node.setAttribute("android:paddingLeft", "0.0dip")
+ node.setAttribute("android:paddingRight", "0.0dip")
+ node.setAttribute("android:paddingBottom", "22.0dip")
+ if (heightIsNotZero && widthIsNotZero) {
+ node.setAttribute("android:layout_height", layoutHeightWidth)
+ node.setAttribute("android:layout_width", layoutHeightWidth)
+ }
+ } else if (timBarItem.containsKey(id)) {
+ node.setAttribute("android:layout_marginBottom", marginBottom)
+ if (!useWiderButtonsSpace) {
+ node.setAttribute("android:paddingBottom", timBarItem.getValue(id))
+ }
+ }
+
+ if (!is_19_17_or_greater && id.equals("@id/youtube_controls_fullscreen_button_stub")) {
+ node.setAttribute("android:layout_width", layoutHeightWidth)
}
}
-
- val (id, height, width) = Triple(
- node.getAttribute("android:id"),
- node.getAttribute("android:layout_height"),
- node.getAttribute("android:layout_width")
- )
- val (heightIsNotZero, widthIsNotZero, isButton) = Triple(
- height != "0.0dip",
- width != "0.0dip",
- id.endsWith("_button") && id != "@id/multiview_button"
- )
-
- // Adjust TimeBar and Chapter bottom padding
- val timBarItem = mutableMapOf(
- "@id/time_bar_chapter_title" to "16.0dip",
- "@id/timestamps_container" to "14.0dip"
- )
-
- val layoutHeightWidth = if (widerButtonsSpace == true)
- "56.0dip"
- else
- "48.0dip"
-
- if (isButton) {
- node.setAttribute("android:layout_marginBottom", marginBottom)
- node.setAttribute("android:paddingLeft", "0.0dip")
- node.setAttribute("android:paddingRight", "0.0dip")
- node.setAttribute("android:paddingBottom", "22.0dip")
- if (heightIsNotZero && widthIsNotZero) {
- node.setAttribute("android:layout_height", layoutHeightWidth)
- node.setAttribute("android:layout_width", layoutHeightWidth)
- }
- } else if (timBarItem.containsKey(id)) {
- node.setAttribute("android:layout_marginBottom", marginBottom)
- if (widerButtonsSpace != true) {
- node.setAttribute("android:paddingBottom", timBarItem.getValue(id))
- }
- }
-
- if (id.equals("@id/youtube_controls_fullscreen_button_stub")) {
- node.setAttribute("android:layout_width", layoutHeightWidth)
}
}
}
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/player/seekbar/SeekbarComponentsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/player/seekbar/SeekbarComponentsPatch.kt
index 49c3fb886..f3f587372 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/player/seekbar/SeekbarComponentsPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/player/seekbar/SeekbarComponentsPatch.kt
@@ -11,12 +11,14 @@ import app.revanced.patcher.util.smali.ExternalLabel
import app.revanced.patches.shared.drawable.addDrawableColorHook
import app.revanced.patches.shared.drawable.drawableColorHookPatch
import app.revanced.patches.shared.mainactivity.onCreateMethod
+import app.revanced.patches.youtube.layout.branding.icon.customBrandingIconPatch
import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE
import app.revanced.patches.youtube.utils.extension.Constants.PATCH_STATUS_CLASS_DESCRIPTOR
import app.revanced.patches.youtube.utils.extension.Constants.PLAYER_CLASS_DESCRIPTOR
import app.revanced.patches.youtube.utils.extension.Constants.PLAYER_PATH
import app.revanced.patches.youtube.utils.flyoutmenu.flyoutMenuHookPatch
import app.revanced.patches.youtube.utils.mainactivity.mainActivityResolvePatch
+import app.revanced.patches.youtube.utils.patch.PatchList.CUSTOM_BRANDING_ICON_FOR_YOUTUBE
import app.revanced.patches.youtube.utils.patch.PatchList.SEEKBAR_COMPONENTS
import app.revanced.patches.youtube.utils.playerButtonsResourcesFingerprint
import app.revanced.patches.youtube.utils.playerButtonsVisibilityFingerprint
@@ -43,6 +45,7 @@ import app.revanced.util.fingerprint.injectLiteralInstructionBooleanCall
import app.revanced.util.fingerprint.matchOrThrow
import app.revanced.util.fingerprint.methodOrThrow
import app.revanced.util.fingerprint.resolvable
+import app.revanced.util.getBooleanOptionValue
import app.revanced.util.getReference
import app.revanced.util.getWalkerMethod
import app.revanced.util.indexOfFirstInstructionOrThrow
@@ -116,6 +119,9 @@ val seekbarComponentsPatch = bytecodePatch(
execute {
+ val restoreOldSplashAnimationIncluded = CUSTOM_BRANDING_ICON_FOR_YOUTUBE.included == true &&
+ customBrandingIconPatch.getBooleanOptionValue("restoreOldSplashAnimationOption").value == true
+
var settingArray = arrayOf(
"PREFERENCE_SCREEN: PLAYER",
"SETTINGS: SEEKBAR_COMPONENTS"
@@ -257,25 +263,29 @@ val seekbarComponentsPatch = bytecodePatch(
"invoke-static/range { p4 .. p5 }, $EXTENSION_SEEKBAR_COLOR_CLASS_DESCRIPTOR->setLinearGradient([I[F)V"
)
- // Don't use the lotte splash screen layout if using custom seekbar.
- arrayOf(
- launchScreenLayoutTypeFingerprint.methodOrThrow(),
- onCreateMethod
- ).forEach { method ->
- method.apply {
- val literalIndex =
- indexOfFirstLiteralInstructionOrThrow(launchScreenLayoutTypeLotteFeatureFlag)
- val resultIndex =
- indexOfFirstInstructionOrThrow(literalIndex, Opcode.MOVE_RESULT)
- val register = getInstruction(resultIndex).registerA
+ if (!restoreOldSplashAnimationIncluded) {
+ // Don't use the lotte splash screen layout if using custom seekbar.
+ arrayOf(
+ launchScreenLayoutTypeFingerprint.methodOrThrow(),
+ onCreateMethod
+ ).forEach { method ->
+ method.apply {
+ val literalIndex =
+ indexOfFirstLiteralInstructionOrThrow(
+ launchScreenLayoutTypeLotteFeatureFlag
+ )
+ val resultIndex =
+ indexOfFirstInstructionOrThrow(literalIndex, Opcode.MOVE_RESULT)
+ val register = getInstruction(resultIndex).registerA
- addInstructions(
- resultIndex + 1,
- """
+ addInstructions(
+ resultIndex + 1,
+ """
invoke-static { v$register }, $EXTENSION_SEEKBAR_COLOR_CLASS_DESCRIPTOR->useLotteLaunchSplashScreen(Z)Z
move-result v$register
"""
- )
+ )
+ }
}
}
@@ -317,7 +327,7 @@ val seekbarComponentsPatch = bytecodePatch(
scaleNode.replaceChild(replacementNode, shapeNode)
}
- if (is_19_25_or_greater) {
+ if (is_19_25_or_greater && !restoreOldSplashAnimationIncluded) {
// Add attribute and styles for splash screen custom color.
// Using a style is the only way to selectively change just the seekbar fill color.
//
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/swipe/controls/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/swipe/controls/Fingerprints.kt
index 1447cc97c..838a52ecf 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/swipe/controls/Fingerprints.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/swipe/controls/Fingerprints.kt
@@ -3,9 +3,14 @@ package app.revanced.patches.youtube.swipe.controls
import app.revanced.patches.youtube.utils.extension.Constants.EXTENSION_PATH
import app.revanced.patches.youtube.utils.resourceid.autoNavScrollCancelPadding
import app.revanced.patches.youtube.utils.resourceid.fullScreenEngagementOverlay
+import app.revanced.util.containsLiteralInstruction
import app.revanced.util.fingerprint.legacyFingerprint
+import app.revanced.util.getReference
+import app.revanced.util.indexOfFirstInstruction
import app.revanced.util.or
import com.android.tools.smali.dexlib2.AccessFlags
+import com.android.tools.smali.dexlib2.Opcode
+import com.android.tools.smali.dexlib2.iface.reference.MethodReference
internal val fullScreenEngagementOverlayFingerprint = legacyFingerprint(
name = "fullScreenEngagementOverlayFingerprint",
@@ -39,7 +44,7 @@ internal val swipeToSwitchVideoFingerprint = legacyFingerprint(
literals = listOf(SWIPE_TO_SWITCH_VIDEO_FEATURE_FLAG),
)
-internal const val WATCH_PANEL_GESTURES_FEATURE_FLAG = 45372793L
+internal const val WATCH_PANEL_GESTURES_PRIMARY_FEATURE_FLAG = 45372793L
/**
* This fingerprint is compatible with YouTube v18.29.38 ~ v19.34.42
@@ -47,10 +52,72 @@ internal const val WATCH_PANEL_GESTURES_FEATURE_FLAG = 45372793L
internal val watchPanelGesturesFingerprint = legacyFingerprint(
name = "watchPanelGesturesFingerprint",
returnType = "V",
- literals = listOf(WATCH_PANEL_GESTURES_FEATURE_FLAG),
+ literals = listOf(WATCH_PANEL_GESTURES_PRIMARY_FEATURE_FLAG),
)
internal val watchPanelGesturesAlternativeFingerprint = legacyFingerprint(
name = "watchPanelGesturesAlternativeFingerprint",
literals = listOf(autoNavScrollCancelPadding),
+)
+
+internal const val WATCH_PANEL_GESTURES_SECONDARY_FEATURE_FLAG = 45619395L
+
+/**
+ * Watch panel gestures in channel bar
+ * This fingerprint is compatible with YouTube v19.15.36 ~
+ */
+internal val watchPanelGesturesChannelBarFingerprint = legacyFingerprint(
+ name = "watchPanelGesturesChannelBarFingerprint",
+ returnType = "Z",
+ accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
+ parameters = listOf("Landroid/view/MotionEvent;"),
+ customFingerprint = { method, _ ->
+ method.definingClass.endsWith("/NextGenWatchLayout;") &&
+ method.name == "onInterceptTouchEvent" &&
+ method.containsLiteralInstruction(WATCH_PANEL_GESTURES_SECONDARY_FEATURE_FLAG)
+ }
+)
+
+/**
+ * fuzzyPatternScanThreshold is required to maintain compatibility with YouTube v18.29.38 ~ v18.32.39.
+ *
+ * TODO: Remove fuzzyPatternScanThreshold if support for YouTube v18.29.38 to v18.32.39 is dropped.
+ */
+internal val playerGestureConfigSyntheticFingerprint = legacyFingerprint(
+ name = "playerGestureConfigSyntheticFingerprint",
+ fuzzyPatternScanThreshold = 5,
+ returnType = "V",
+ accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
+ parameters = listOf("Ljava/lang/Object;"),
+ opcodes = listOf(
+ Opcode.SGET_OBJECT,
+ Opcode.INVOKE_VIRTUAL,
+ Opcode.MOVE_RESULT,
+ Opcode.IF_EQZ,
+ Opcode.IF_EQZ,
+ Opcode.IGET_OBJECT,
+ Opcode.INVOKE_INTERFACE,
+ Opcode.MOVE_RESULT_OBJECT,
+ Opcode.INVOKE_VIRTUAL, // playerGestureConfig.downAndOutLandscapeAllowed
+ Opcode.MOVE_RESULT,
+ Opcode.CHECK_CAST,
+ Opcode.IPUT_BOOLEAN,
+ Opcode.INVOKE_INTERFACE,
+ Opcode.MOVE_RESULT_OBJECT,
+ Opcode.INVOKE_VIRTUAL, // playerGestureConfig.downAndOutPortraitAllowed
+ Opcode.MOVE_RESULT,
+ Opcode.IPUT_BOOLEAN,
+ Opcode.RETURN_VOID,
+ ),
+ customFingerprint = { method, classDef ->
+ // This method is always called "a" because this kind of class always has a single method.
+ method.name == "a" &&
+ classDef.methods.count() == 2 &&
+ method.indexOfFirstInstruction {
+ val reference = getReference()
+ reference?.definingClass == "Lcom/google/android/libraries/youtube/innertube/model/media/PlayerConfigModel;" &&
+ reference.parameterTypes.isEmpty() &&
+ reference.returnType == "Z"
+ } >= 0
+ },
)
\ No newline at end of file
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/swipe/controls/SwipeControlsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/swipe/controls/SwipeControlsPatch.kt
index 3d404f4b7..af0c104b5 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/swipe/controls/SwipeControlsPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/swipe/controls/SwipeControlsPatch.kt
@@ -1,6 +1,7 @@
package app.revanced.patches.youtube.swipe.controls
import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
+import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
import app.revanced.patcher.extensions.InstructionExtensions.removeInstruction
@@ -16,6 +17,7 @@ import app.revanced.patches.youtube.utils.mainactivity.mainActivityResolvePatch
import app.revanced.patches.youtube.utils.patch.PatchList.SWIPE_CONTROLS
import app.revanced.patches.youtube.utils.playertype.playerTypeHookPatch
import app.revanced.patches.youtube.utils.playservice.is_19_09_or_greater
+import app.revanced.patches.youtube.utils.playservice.is_19_15_or_greater
import app.revanced.patches.youtube.utils.playservice.is_19_23_or_greater
import app.revanced.patches.youtube.utils.playservice.is_19_36_or_greater
import app.revanced.patches.youtube.utils.playservice.versionCheckPatch
@@ -28,9 +30,11 @@ import app.revanced.patches.youtube.utils.settings.settingsPatch
import app.revanced.util.ResourceGroup
import app.revanced.util.copyResources
import app.revanced.util.fingerprint.injectLiteralInstructionBooleanCall
+import app.revanced.util.fingerprint.matchOrThrow
import app.revanced.util.fingerprint.methodOrThrow
import app.revanced.util.fingerprint.mutableClassOrThrow
import app.revanced.util.getReference
+import app.revanced.util.getWalkerMethod
import app.revanced.util.indexOfFirstInstructionOrThrow
import app.revanced.util.indexOfFirstLiteralInstruction
import app.revanced.util.indexOfFirstLiteralInstructionOrThrow
@@ -106,7 +110,7 @@ val swipeControlsPatch = bytecodePatch(
var settingArray = arrayOf(
"PREFERENCE_SCREEN: SWIPE_CONTROLS",
- "SETTINGS: DISABLE_WATCH_PANEL_GESTURES"
+ "SETTINGS: DISABLE_SWIPE_TO_ENTER_FULLSCREEN_MODE_BELOW_THE_PLAYER"
)
// region patch for disable HDR auto brightness
@@ -142,12 +146,12 @@ val swipeControlsPatch = bytecodePatch(
// endregion
- // region patch for disable watch panel gestures
+ // region patch for disable swipe to enter fullscreen mode (below the player)
if (!is_19_36_or_greater) {
watchPanelGesturesFingerprint.injectLiteralInstructionBooleanCall(
- WATCH_PANEL_GESTURES_FEATURE_FLAG,
- "$EXTENSION_SWIPE_CONTROLS_PATCH_CLASS_DESCRIPTOR->disableWatchPanelGestures()Z"
+ WATCH_PANEL_GESTURES_PRIMARY_FEATURE_FLAG,
+ "$EXTENSION_SWIPE_CONTROLS_PATCH_CLASS_DESCRIPTOR->disableSwipeToEnterFullscreenModeBelowThePlayer()Z"
)
} else {
watchPanelGesturesAlternativeFingerprint.methodOrThrow().apply {
@@ -187,7 +191,7 @@ val swipeControlsPatch = bytecodePatch(
addInstructionsWithLabels(
targetIndex, """
- invoke-static {}, $EXTENSION_SWIPE_CONTROLS_PATCH_CLASS_DESCRIPTOR->disableWatchPanelGestures()Z
+ invoke-static {}, $EXTENSION_SWIPE_CONTROLS_PATCH_CLASS_DESCRIPTOR->disableSwipeToEnterFullscreenModeBelowThePlayer()Z
move-result v${fieldInstruction.registerA}
if-eqz v${fieldInstruction.registerA}, :disable
iget-object v${fieldInstruction.registerA}, v${fieldInstruction.registerB}, $fieldReference
@@ -197,6 +201,39 @@ val swipeControlsPatch = bytecodePatch(
}
}
+ if (is_19_15_or_greater) {
+ watchPanelGesturesChannelBarFingerprint.injectLiteralInstructionBooleanCall(
+ WATCH_PANEL_GESTURES_SECONDARY_FEATURE_FLAG,
+ "$EXTENSION_SWIPE_CONTROLS_PATCH_CLASS_DESCRIPTOR->disableSwipeToEnterFullscreenModeBelowThePlayer()Z"
+ )
+ }
+
+ // endregion
+
+ // region patch for disable swipe to enter fullscreen mode (in the player) and disable swipe to exit fullscreen mode
+
+ playerGestureConfigSyntheticFingerprint.matchOrThrow().let {
+ val endIndex = it.patternMatch!!.endIndex
+
+ mapOf(
+ 3 to "disableSwipeToEnterFullscreenModeInThePlayer",
+ 9 to "disableSwipeToExitFullscreenMode"
+ ).forEach { (offSet, methodName) ->
+ it.getWalkerMethod(endIndex - offSet).apply {
+ val index = implementation!!.instructions.lastIndex
+ val register = getInstruction(index).registerA
+
+ addInstructions(
+ index,
+ """
+ invoke-static {v$register}, $EXTENSION_SWIPE_CONTROLS_PATCH_CLASS_DESCRIPTOR->$methodName(Z)Z
+ move-result v$register
+ """
+ )
+ }
+ }
+ }
+
// endregion
// region copy resources
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/extension/SharedExtensionPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/extension/SharedExtensionPatch.kt
index bab6ee3da..263d4fb8c 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/extension/SharedExtensionPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/extension/SharedExtensionPatch.kt
@@ -2,8 +2,10 @@ package app.revanced.patches.youtube.utils.extension
import app.revanced.patches.shared.extension.sharedExtensionPatch
import app.revanced.patches.youtube.utils.extension.hooks.applicationInitHook
+import app.revanced.patches.youtube.utils.extension.hooks.mainActivityBaseContextHook
// TODO: Move this to a "Hook.kt" file. Same for other extension hook patches.
val sharedExtensionPatch = sharedExtensionPatch(
applicationInitHook,
+ mainActivityBaseContextHook,
)
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/extension/hooks/MainActivityBaseContextHook.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/extension/hooks/MainActivityBaseContextHook.kt
new file mode 100644
index 000000000..1f17ed305
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/extension/hooks/MainActivityBaseContextHook.kt
@@ -0,0 +1,36 @@
+package app.revanced.patches.youtube.utils.extension.hooks
+
+import app.revanced.patches.shared.extension.extensionHook
+import app.revanced.util.getReference
+import app.revanced.util.indexOfFirstInstructionOrThrow
+import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction
+import com.android.tools.smali.dexlib2.iface.reference.MethodReference
+
+private var attachBaseContextIndex = -1
+
+internal val mainActivityBaseContextHook = extensionHook(
+ insertIndexResolver = { method ->
+ attachBaseContextIndex = method.indexOfFirstInstructionOrThrow {
+ getReference()?.name == "attachBaseContext"
+ }
+
+ attachBaseContextIndex + 1
+ },
+ contextRegisterResolver = { method ->
+ val overrideInstruction =
+ method.implementation!!.instructions.elementAt(attachBaseContextIndex)
+ as FiveRegisterInstruction
+ "v${overrideInstruction.registerD}"
+ },
+) {
+ returns("V")
+ parameters("Landroid/content/Context;")
+ custom { method, classDef ->
+ method.name == "attachBaseContext" &&
+ (
+ classDef.endsWith("/MainActivity;") ||
+ // Old versions of YouTube called this class "WatchWhileActivity" instead.
+ classDef.endsWith("/WatchWhileActivity;")
+ )
+ }
+}
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/streamingdata/SpoofStreamingDataPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/streamingdata/SpoofStreamingDataPatch.kt
index c82bbb612..a2ba66890 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/streamingdata/SpoofStreamingDataPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/streamingdata/SpoofStreamingDataPatch.kt
@@ -1,7 +1,5 @@
package app.revanced.patches.youtube.utils.fix.streamingdata
-import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
-import app.revanced.patches.shared.extension.Constants.PATCHES_PATH
import app.revanced.patches.shared.spoof.streamingdata.baseSpoofStreamingDataPatch
import app.revanced.patches.shared.spoof.useragent.baseSpoofUserAgentPatch
import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE
@@ -9,7 +7,6 @@ import app.revanced.patches.youtube.utils.compatibility.Constants.YOUTUBE_PACKAG
import app.revanced.patches.youtube.utils.patch.PatchList.SPOOF_STREAMING_DATA
import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference
import app.revanced.patches.youtube.utils.settings.settingsPatch
-import app.revanced.util.findMethodOrThrow
val spoofStreamingDataPatch = baseSpoofStreamingDataPatch(
{
@@ -21,13 +18,6 @@ val spoofStreamingDataPatch = baseSpoofStreamingDataPatch(
)
},
{
- findMethodOrThrow("$PATCHES_PATH/PatchStatus;") {
- name == "SpoofStreamingDataAndroidOnlyDefaultBoolean"
- }.replaceInstruction(
- 0,
- "const/4 v0, 0x1"
- )
-
addPreference(
arrayOf(
"SETTINGS: SPOOF_STREAMING_DATA"
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fullscreen/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fullscreen/Fingerprints.kt
new file mode 100644
index 000000000..17f94470f
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fullscreen/Fingerprints.kt
@@ -0,0 +1,35 @@
+package app.revanced.patches.youtube.utils.fullscreen
+
+import app.revanced.util.containsLiteralInstruction
+import app.revanced.util.fingerprint.legacyFingerprint
+import app.revanced.util.or
+import com.android.tools.smali.dexlib2.AccessFlags
+import com.android.tools.smali.dexlib2.Opcode
+
+private const val NEXT_GEN_WATCH_LAYOUT_CLASS_DESCRIPTOR =
+ "Lcom/google/android/apps/youtube/app/watch/nextgenwatch/ui/NextGenWatchLayout;"
+
+internal val nextGenWatchLayoutConstructorFingerprint = legacyFingerprint(
+ name = "nextGenWatchLayoutConstructorFingerprint",
+ returnType = "V",
+ accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR,
+ parameters = listOf("Landroid/content/Context;", "Landroid/util/AttributeSet;", "I"),
+ opcodes = listOf(Opcode.CHECK_CAST),
+ customFingerprint = { method, _ ->
+ method.definingClass == NEXT_GEN_WATCH_LAYOUT_CLASS_DESCRIPTOR
+ },
+)
+
+internal val nextGenWatchLayoutFullscreenModeFingerprint = legacyFingerprint(
+ name = "nextGenWatchLayoutFullscreenModeFingerprint",
+ returnType = "V",
+ accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
+ parameters = listOf("I"),
+ opcodes = listOf(Opcode.INVOKE_DIRECT),
+ customFingerprint = { method, _ ->
+ method.definingClass == NEXT_GEN_WATCH_LAYOUT_CLASS_DESCRIPTOR &&
+ method.containsLiteralInstruction(32)
+ },
+)
+
+
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fullscreen/FullscreenButtonHookPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fullscreen/FullscreenButtonHookPatch.kt
new file mode 100644
index 000000000..489b38a11
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fullscreen/FullscreenButtonHookPatch.kt
@@ -0,0 +1,139 @@
+package app.revanced.patches.youtube.utils.fullscreen
+
+import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
+import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
+import app.revanced.patcher.patch.PatchException
+import app.revanced.patcher.patch.bytecodePatch
+import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
+import app.revanced.patches.youtube.utils.extension.Constants.EXTENSION_PATH
+import app.revanced.patches.youtube.utils.extension.sharedExtensionPatch
+import app.revanced.util.addStaticFieldToExtension
+import app.revanced.util.findMethodOrThrow
+import app.revanced.util.fingerprint.methodOrThrow
+import app.revanced.util.getReference
+import app.revanced.util.indexOfFirstInstructionOrThrow
+import app.revanced.util.indexOfFirstInstructionReversedOrThrow
+import com.android.tools.smali.dexlib2.Opcode
+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.reference.MethodReference
+import com.android.tools.smali.dexlib2.iface.reference.TypeReference
+
+private const val EXTENSION_VIDEO_UTILS_CLASS_DESCRIPTOR =
+ "$EXTENSION_PATH/utils/VideoUtils;"
+
+internal lateinit var enterFullscreenMethod: MutableMethod
+
+val fullscreenButtonHookPatch = bytecodePatch(
+ description = "fullscreenButtonHookPatch"
+) {
+
+ dependsOn(sharedExtensionPatch)
+
+ execute {
+ val (referenceClass, fullscreenActionClass) = with(
+ nextGenWatchLayoutFullscreenModeFingerprint.methodOrThrow()
+ ) {
+ val targetIndex = indexOfFirstInstructionReversedOrThrow {
+ opcode == Opcode.INVOKE_DIRECT &&
+ getReference()?.parameterTypes?.size == 2
+ }
+ val targetReference =
+ getInstruction(targetIndex).reference as MethodReference
+
+ Pair(targetReference.definingClass, targetReference.parameterTypes[1].toString())
+ }
+
+ val (enterFullscreenReference, exitFullscreenReference, opcodeName) =
+ with(findMethodOrThrow(referenceClass) { parameters == listOf("I") }) {
+ val enterFullscreenIndex = indexOfFirstInstructionOrThrow {
+ val reference = getReference()
+ reference?.returnType == "V" &&
+ reference.definingClass == fullscreenActionClass &&
+ reference.parameterTypes.size == 0
+ }
+ val exitFullscreenIndex = indexOfFirstInstructionReversedOrThrow {
+ val reference = getReference()
+ reference?.returnType == "V" &&
+ reference.definingClass == fullscreenActionClass &&
+ reference.parameterTypes.size == 0
+ }
+
+ val enterFullscreenReference =
+ getInstruction(enterFullscreenIndex).reference
+ val exitFullscreenReference =
+ getInstruction(exitFullscreenIndex).reference
+ val opcode = getInstruction(enterFullscreenIndex).opcode
+
+ val enterFullscreenClass =
+ (enterFullscreenReference as MethodReference).definingClass
+
+ enterFullscreenMethod = if (opcode == Opcode.INVOKE_INTERFACE) {
+ classes.find { classDef -> classDef.interfaces.contains(enterFullscreenClass) }
+ ?.let { classDef ->
+ proxy(classDef)
+ .mutableClass
+ .methods
+ .find { method -> method.name == enterFullscreenReference.name }
+ } ?: throw PatchException("No matching classes: $enterFullscreenClass")
+ } else {
+ findMethodOrThrow(enterFullscreenClass) {
+ name == enterFullscreenReference.name
+ }
+ }
+
+ Triple(
+ enterFullscreenReference,
+ exitFullscreenReference,
+ opcode.name
+ )
+ }
+
+ nextGenWatchLayoutConstructorFingerprint.methodOrThrow().apply {
+ val targetIndex = indexOfFirstInstructionReversedOrThrow {
+ opcode == Opcode.CHECK_CAST &&
+ getReference()?.type == fullscreenActionClass
+ }
+ val targetRegister = getInstruction(targetIndex).registerA
+
+ addInstruction(
+ targetIndex + 1,
+ "sput-object v$targetRegister, $EXTENSION_VIDEO_UTILS_CLASS_DESCRIPTOR->fullscreenActionClass:$fullscreenActionClass"
+ )
+
+ val enterFullscreenModeSmaliInstructions =
+ """
+ if-eqz v0, :ignore
+ $opcodeName {v0}, $enterFullscreenReference
+ :ignore
+ return-void
+ """
+
+ val exitFullscreenModeSmaliInstructions =
+ """
+ if-eqz v0, :ignore
+ $opcodeName {v0}, $exitFullscreenReference
+ :ignore
+ return-void
+ """
+
+ addStaticFieldToExtension(
+ EXTENSION_VIDEO_UTILS_CLASS_DESCRIPTOR,
+ "enterFullscreenMode",
+ "fullscreenActionClass",
+ fullscreenActionClass,
+ enterFullscreenModeSmaliInstructions,
+ false
+ )
+
+ addStaticFieldToExtension(
+ EXTENSION_VIDEO_UTILS_CLASS_DESCRIPTOR,
+ "exitFullscreenMode",
+ "fullscreenActionClass",
+ fullscreenActionClass,
+ exitFullscreenModeSmaliInstructions,
+ false
+ )
+ }
+ }
+}
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/patch/PatchList.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/patch/PatchList.kt
index 5ebdc1893..2e944d1ba 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/patch/PatchList.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/patch/PatchList.kt
@@ -23,7 +23,7 @@ internal enum class PatchList(
),
CHANGE_SHARE_SHEET(
"Change share sheet",
- "Add option to change from in-app share sheet to system share sheet."
+ "Adds an option to change the in-app share sheet to the system share sheet."
),
CHANGE_START_PAGE(
"Change start page",
@@ -147,7 +147,7 @@ internal enum class PatchList(
),
HIDE_SHORTCUTS(
"Hide shortcuts",
- "Remove, at compile time, the app shortcuts that appears when app icon is long pressed."
+ "Remove, at compile time, the app shortcuts that appears when the app icon is long pressed."
),
HOOK_YOUTUBE_MUSIC_ACTIONS(
"Hook YouTube Music actions",
@@ -235,7 +235,7 @@ internal enum class PatchList(
),
TOOLBAR_COMPONENTS(
"Toolbar components",
- "Adds options to hide or change components located on the toolbar, such as toolbar buttons, search bar, and header."
+ "Adds options to hide or change components located on the toolbar, such as the search bar, header, and toolbar buttons."
),
TRANSLATIONS_FOR_YOUTUBE(
"Translations for YouTube",
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/playercontrols/PlayerControlsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/playercontrols/PlayerControlsPatch.kt
index 8a648f0f4..e0e54f204 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/playercontrols/PlayerControlsPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/playercontrols/PlayerControlsPatch.kt
@@ -9,8 +9,11 @@ import app.revanced.patcher.patch.resourcePatch
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
import app.revanced.patches.youtube.utils.extension.Constants.UTILS_PATH
import app.revanced.patches.youtube.utils.extension.sharedExtensionPatch
+import app.revanced.patches.youtube.utils.fullscreen.enterFullscreenMethod
+import app.revanced.patches.youtube.utils.fullscreen.fullscreenButtonHookPatch
import app.revanced.patches.youtube.utils.playerButtonsResourcesFingerprint
import app.revanced.patches.youtube.utils.playerButtonsVisibilityFingerprint
+import app.revanced.patches.youtube.utils.playservice.is_19_23_or_greater
import app.revanced.patches.youtube.utils.playservice.is_19_25_or_greater
import app.revanced.patches.youtube.utils.playservice.versionCheckPatch
import app.revanced.patches.youtube.utils.resourceid.sharedResourceIdPatch
@@ -46,6 +49,7 @@ private val playerControlsBytecodePatch = bytecodePatch(
sharedExtensionPatch,
sharedResourceIdPatch,
versionCheckPatch,
+ fullscreenButtonHookPatch,
)
execute {
@@ -114,6 +118,18 @@ private val playerControlsBytecodePatch = bytecodePatch(
// endregion
+ // region patch for fix buttons do not hide immediately when fullscreen button is clicked
+
+ // Reproduced only in RVX
+ if (is_19_23_or_greater) {
+ enterFullscreenMethod.addInstruction(
+ 0,
+ "invoke-static {}, $EXTENSION_PLAYER_CONTROLS_CLASS_DESCRIPTOR->changeVisibilityNegatedImmediately()V"
+ )
+ }
+
+ // endregion
+
// region patch initialize of overlay button or SponsorBlock button
mapOf(
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/settings/SettingsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/settings/SettingsPatch.kt
index 13e59bb69..caa5ebb0c 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/settings/SettingsPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/settings/SettingsPatch.kt
@@ -54,9 +54,9 @@ private val settingsBytecodePatch = bytecodePatch(
addInstructions(
index + 1, """
- invoke-static {v$register}, $EXTENSION_THEME_METHOD_DESCRIPTOR
- return-object v$register
- """
+ invoke-static {v$register}, $EXTENSION_THEME_METHOD_DESCRIPTOR
+ return-object v$register
+ """
)
removeInstruction(index)
}
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/playback/VideoPlaybackPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/playback/VideoPlaybackPatch.kt
index ccebc248a..895d7b505 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/video/playback/VideoPlaybackPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/playback/VideoPlaybackPatch.kt
@@ -173,7 +173,7 @@ val videoPlaybackPatch = bytecodePatch(
}
hookBackgroundPlayVideoInformation("$EXTENSION_PLAYBACK_SPEED_CLASS_DESCRIPTOR->newVideoStarted(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;JZ)V")
- hookPlayerResponseVideoId("$EXTENSION_PLAYBACK_SPEED_CLASS_DESCRIPTOR->fetchPlaylistData(Ljava/lang/String;Z)V")
+ hookPlayerResponseVideoId("$EXTENSION_PLAYBACK_SPEED_CLASS_DESCRIPTOR->fetchMusicRequest(Ljava/lang/String;Z)V")
updatePatchStatus(PATCH_STATUS_CLASS_DESCRIPTOR, "RememberPlaybackSpeed")
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/playerresponse/PlayerResponseMethodHookPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/playerresponse/PlayerResponseMethodHookPatch.kt
index 901ef8d7a..ae9c22223 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/video/playerresponse/PlayerResponseMethodHookPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/playerresponse/PlayerResponseMethodHookPatch.kt
@@ -70,7 +70,7 @@ val playerResponseMethodHookPatch = bytecodePatch(
"""
invoke-static {$registerVideoId, $registerPlayerParameter, $registerPlaylistId, $registerIsShortAndOpeningOrPlaying}, $hook
move-result-object $registerPlayerParameter
- """,
+ """,
)
numberOfInstructionsAdded += 2
}
diff --git a/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt b/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt
index 9f9c0989d..cd58c6e34 100644
--- a/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt
+++ b/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt
@@ -263,6 +263,34 @@ fun MutableMethod.injectLiteralInstructionViewCall(
)
}
+fun BytecodePatchContext.replaceLiteralInstructionCall(
+ originalLiteral: Long,
+ replaceLiteral: Long
+) {
+ classes.forEach { classDef ->
+ classDef.methods.forEach { method ->
+ method.implementation.apply {
+ this?.instructions?.forEachIndexed { _, instruction ->
+ if (instruction.opcode != Opcode.CONST)
+ return@forEachIndexed
+ if ((instruction as Instruction31i).wideLiteral != originalLiteral)
+ return@forEachIndexed
+
+ proxy(classDef)
+ .mutableClass
+ .findMutableMethodOf(method).apply {
+ val index = indexOfFirstLiteralInstructionOrThrow(originalLiteral)
+ val register =
+ (instruction as OneRegisterInstruction).registerA
+
+ replaceInstruction(index, "const v$register, $replaceLiteral")
+ }
+ }
+ }
+ }
+ }
+}
+
fun BytecodePatchContext.replaceLiteralInstructionCall(
literal: Long,
smaliInstruction: String
diff --git a/patches/src/main/kotlin/app/revanced/util/ResourceUtils.kt b/patches/src/main/kotlin/app/revanced/util/ResourceUtils.kt
index 89be81890..9f172b065 100644
--- a/patches/src/main/kotlin/app/revanced/util/ResourceUtils.kt
+++ b/patches/src/main/kotlin/app/revanced/util/ResourceUtils.kt
@@ -20,6 +20,10 @@ private val classLoader = object {}.javaClass.classLoader
fun Patch<*>.getStringOptionValue(key: String) =
options[key] as Option
+@Suppress("UNCHECKED_CAST")
+fun Patch<*>.getBooleanOptionValue(key: String) =
+ options[key] as Option
+
fun Option.valueOrThrow() = value
?: throw PatchException("Invalid patch option: $title.")
diff --git a/patches/src/main/kotlin/app/revanced/util/fingerprint/LegacyFingerprint.kt b/patches/src/main/kotlin/app/revanced/util/fingerprint/LegacyFingerprint.kt
index ef7e9dbcb..36dc8bdb2 100644
--- a/patches/src/main/kotlin/app/revanced/util/fingerprint/LegacyFingerprint.kt
+++ b/patches/src/main/kotlin/app/revanced/util/fingerprint/LegacyFingerprint.kt
@@ -134,16 +134,17 @@ fun Pair.injectLiteralInstructionViewCall(
internal fun legacyFingerprint(
name: String,
+ fuzzyPatternScanThreshold: Int = 0,
accessFlags: Int? = null,
returnType: String? = null,
parameters: List? = null,
opcodes: List? = null,
strings: List? = null,
literals: List? = null,
- customFingerprint: ((methodDef: Method, classDef: ClassDef) -> Boolean)? = null
+ customFingerprint: ((methodDef: Method, classDef: ClassDef) -> Boolean)? = null,
) = Pair(
name,
- fingerprint {
+ fingerprint(fuzzyPatternScanThreshold = fuzzyPatternScanThreshold) {
if (accessFlags != null) {
accessFlags(accessFlags)
}
diff --git a/patches/src/main/resources/music/settings/host/values/arrays.xml b/patches/src/main/resources/music/settings/host/values/arrays.xml
index 81986ed4c..c1adaccde 100644
--- a/patches/src/main/resources/music/settings/host/values/arrays.xml
+++ b/patches/src/main/resources/music/settings/host/values/arrays.xml
@@ -14,6 +14,16 @@
- FEmusic_library_landing
- FEmusic_library_corpus_artists
+
+ - @string/revanced_disable_music_video_in_album_redirect_type_entry_redirect
+ - @string/revanced_disable_music_video_in_album_redirect_type_entry_on_click
+ - @string/revanced_disable_music_video_in_album_redirect_type_entry_on_long_click
+
+
+ - REDIRECT
+ - ON_CLICK
+ - ON_LONG_CLICK
+
- @string/revanced_extended_settings_export_as_file
- @string/revanced_extended_settings_import_as_file
@@ -67,12 +77,10 @@
- @string/revanced_spoof_streaming_data_type_entry_android_vr
- - @string/revanced_spoof_streaming_data_type_entry_ios
- - @string/revanced_spoof_streaming_data_type_entry_ios_music
+ - @string/revanced_spoof_streaming_data_type_entry_android_music
- ANDROID_VR
- - IOS
- - IOS_MUSIC
+ - ANDROID_MUSIC
diff --git a/patches/src/main/resources/music/settings/host/values/strings.xml b/patches/src/main/resources/music/settings/host/values/strings.xml
index d04d6e15b..c3479c6a5 100644
--- a/patches/src/main/resources/music/settings/host/values/strings.xml
+++ b/patches/src/main/resources/music/settings/host/values/strings.xml
@@ -420,6 +420,17 @@ Click to see how to issue a API key."
Disables Cairo splash animation when the app starts up.
Disable DRC audio
Disables DRC (Dynamic Range Compression) applied to audio.
+ Disable music video in album
+ "When a non-premium user plays a song included in an album, the music video is sometimes played instead of the official song.
+
+Find the official song if a music video is detected playing from an album.
+
+• Powered by Piped Instance API."
+ Redirection type
+ Specifies how to redirect to official song.
+ Redirect
+ Tap Audio / Video toggle
+ Tap and hold Audio / Video toggle
Enable debug logging
Prints the debug log.
Enable debug buffer logging
@@ -451,11 +462,11 @@ Tap the continue button and allow optimization changes."
Spoof client
"Spoof the client to prevent playback issues.
-※ When used with 'Spoofing streaming data', playback issues may occur."
+• When used with 'Spoofing streaming data', playback issues may occur."
Default client
- "Defines a default client to spoofing.
+ "Defines a default client for spoofing.
-※ When using the Android client, it is recommended to use it with 'Spoof app version'."
+• When using the Android client, it is recommended to also use 'Spoof app version'."
Android Music 4.27.53
Android Music 5.29.53
iOS Music 6.21
@@ -463,15 +474,13 @@ Tap the continue button and allow optimization changes."
Spoof streaming data
"Spoof the streaming data to prevent playback issues.
-※ When used with 'Spoof client', playback issues may occur."
+• When used with 'Spoof client', playback issues may occur."
Default client
Defines a default client that fetches streaming data.
Show in Stats for nerds
Shows the client used to fetch streaming data in Stats for nerds.
- iOS
- iOS Music
- Android TV
Android VR
+ Android Music
Sanitize sharing links
Removes tracking query parameters from URLs when sharing links.
diff --git a/patches/src/main/resources/music/theme/drawable/revanced_header_gradient.xml b/patches/src/main/resources/music/theme/drawable/revanced_header_gradient.xml
new file mode 100644
index 000000000..f024c86f5
--- /dev/null
+++ b/patches/src/main/resources/music/theme/drawable/revanced_header_gradient.xml
@@ -0,0 +1,5 @@
+
+
+
+
\ No newline at end of file
diff --git a/patches/src/main/resources/music/translations/el-rGR/strings.xml b/patches/src/main/resources/music/translations/el-rGR/strings.xml
index e332009ef..28a5a435f 100644
--- a/patches/src/main/resources/music/translations/el-rGR/strings.xml
+++ b/patches/src/main/resources/music/translations/el-rGR/strings.xml
@@ -375,6 +375,17 @@
Απενεργοποίηση των εφέ θέματος Cairo κατά την εκκίνηση της εφαρμογής.
Απενεργοποίηση ήχου DRC
Απενεργοποίηση του DRC (Συμπίεση Δυναμικού Εύρους) που εφαρμόζεται στον ήχο.
+ Απενεργοποίηση μουσικών βίντεο στα άλμπουμ
+ "Όταν ένας μη-premium χρήστης παίζει ένα τραγούδι που περιλαμβάνεται σε ένα άλμπουμ, μερικές φορές αναπαράγεται το μουσικό βίντεο αντί για το επίσημο τραγούδι.
+
+Αν ανιχνεύεται αναπαραγωγή ενός μουσικού βίντεο που περιλαμβάνεται σε άλμπουμ, ανακατευθύνεται στο επίσημο τραγούδι.
+
+• Χρησιμοποιείται το Piped Instance API."
+ Τύπος ανακατεύθυνσης
+ Καθορισμός του τρόπου ανακατεύθυνσης στο επίσημο τραγούδι.
+ Ανακατεύθυνση
+ Πάτημα εναλλαγής ήχου / βίντεο
+ Παρατεταμένο πάτημα εναλλαγής ήχου / βίντεο
Ενεργοποίηση καταγραφής σφαλμάτων
Εκτύπωση του αρχείου καταγραφής σφαλμάτων.
Συμπερίληψη του buffer στην καταγραφή
@@ -406,7 +417,7 @@
Προεπιλεγμένο πρόγραμμα πελάτη
"Καθορισμός ενός προεπιλεγμένου πρόγραμμα πελάτη για παραποίηση.
-※ Όταν ορίζεται πρόγραμμα πελάτη τύπου Android, συνιστάται να το χρησιμοποιήσετε μαζί με την λειτουργία «Παραποίηση έκδοσης εφαρμογής»."
+※ Όταν ορίζεται πρόγραμμα πελάτη Android, συνιστάται να το χρησιμοποιήσετε μαζί με την λειτουργία «Παραποίηση έκδοσης εφαρμογής»."
Android Music 4.27.53
Android Music 5.29.53
iOS Music 6.21
@@ -418,10 +429,8 @@
Καθορισμός ενός προεπιλεγμένου προγράμματος-πελάτη για την λήψη δεδομένων ροής.
Εμφάνιση στο «Στατιστικά για σπασίκλες»
Εμφάνιση του προγράμματος πελάτη που χρησιμοποιείται για τη λήψη δεδομένων ροής στο μενού «Στατιστικά για σπασίκλες».
- iOS
- iOS Music
- Android TV
Android VR
+ Android Music
Καθαρισμός συνδέσμων κοινοποίησης
Αφαίρεση των παραμέτρων παρακολούθησης από τις διευθύνσεις URL κατά την κοινοποίηση συνδέσμων.
Άνοιγμα ρυθμίσεων προεπιλεγμένων εφαρμογών
diff --git a/patches/src/main/resources/music/translations/es-rES/strings.xml b/patches/src/main/resources/music/translations/es-rES/strings.xml
index dbc4586da..a6d0ee1c7 100644
--- a/patches/src/main/resources/music/translations/es-rES/strings.xml
+++ b/patches/src/main/resources/music/translations/es-rES/strings.xml
@@ -68,7 +68,7 @@ Descarga %2$s desde el sitio web."
"Activa el diálogo compacto en el teléfono.
Problemas conocidos:
-- La carátula del álbum en la estantería de la biblioteca también se hace más pequeña.
+- Las carátulas de los álbumes en la página de biblioteca se hacen pequeñas cuando se organizan por cuadrícula.
- El diseño del temporizador puede parecer inusual."
Añadir interruptor para recortar silencios
"Añade un interruptor para recortar silencios en el menú desplegable de velocidad de reproducción.
@@ -89,6 +89,7 @@ Esta función aún está en desarrollo, por lo que puede ser inestable."Ocultar menú de ir a episodios
Ocultar menú de ir al podcast
Ocultar menú Ayuda & Comentarios
+ Ocultar el pin al menú de marcación rápida
Ocultar menú de reproducción siguiente
Ocultar menú de calidad
Ocultar menú de eliminar de la biblioteca
@@ -103,6 +104,7 @@ Esta función aún está en desarrollo, por lo que puede ser inestable."Ocultar menú de Iniciar radio
Ocultar menú Estadísticas para Nerds
Ocultar menú Suscribirse / Desuscribirse
+ Ocultar Desfijar del menú de marcación rápida
Ocultar menú de vista de créditos de canción
Continuar viendo
Continúa el vídeo desde el tiempo actual cuando se cambia a YouTube.
@@ -368,6 +370,19 @@ Toca para ver cómo crear una clave de API."
Cambia la hoja de compartir en la app a la hoja de compartir del sistema.
Desactiva la animación Cairo
Deshabilita la animación de bienvenida \"Cairo\" cuando se inicia la aplicación.
+ Desactivar audio DRC
+ Deshabilita DRC (Dynamic Range Compression) aplicado al audio.
+ Desactivar vídeo de música en el álbum
+ "Cuando un usuario no premium escoge una canción incluida en un álbum, un vídeo musical a veces se muestra en lugar de la canción original.
+
+Si este video se detecta durante la reproducción, será redireccionado a la canción original.
+
+Se usa una instancia redireccionada, pero la API puede no estar disponible en algunas regiones."
+ Tipo de redirección
+ Especifica cómo redirigir a la canción oficial.
+ Redirigir
+ Interruptor de Audio / Video
+ Mantén pulsado el interruptor de Audio / Video
Activar registro de depuración
Imprime el registro de depuración.
Incluir búfer en registro de depuración
@@ -395,6 +410,23 @@ Limitaciones:
• Código de audio OPUS puede no ser compatible.
• La miniatura de la barra de Seekbar puede no estar presente.
• El historial de la vista no funciona con una cuenta de marca."
+ Cliente por defecto
+ "Define un cliente predeterminado a falsear.
+
+※ Al usar el cliente Android, se recomienda usarlo con 'Versión de aplicación Spoof'."
+ Android Music 4.27.53
+ Android Music 5.29.53
+ iOS Music 6.21
+ Falsificar datos de transmisión
+ "Falsificar los datos de streaming para evitar problemas de reproducción.
+
+※ Cuando se utiliza con 'Cliente de spoof', problemas de reproducción pueden producirse."
+ Cliente por defecto
+ Define un cliente por defecto que obtiene datos de streaming.
+ Mostrar en estadísticas para nerds
+ Muestra el cliente utilizado para obtener datos de streaming en Estadísticas para nerds.
+ Android VR
+ Android Music
Desinfectar enlaces compartidos
Elimina los parámetros de consulta de seguimiento de las URL al compartir enlaces.
Abrir ajustes predeterminados de la app
diff --git a/patches/src/main/resources/music/translations/hu-rHU/strings.xml b/patches/src/main/resources/music/translations/hu-rHU/strings.xml
index ccac0dbb1..87bd26fb0 100644
--- a/patches/src/main/resources/music/translations/hu-rHU/strings.xml
+++ b/patches/src/main/resources/music/translations/hu-rHU/strings.xml
@@ -275,7 +275,7 @@ Előfordulhat, hogy egyes funkciók nem működnek megfelelően a régi lejátsz
A nem tetszések százalékos arányát jeleníti meg a nem tetszések száma helyett.
Kompakt kedvelés gomb
Elrejti a kedvelés gomb elválasztóját.
- Mutassa a becsült kedveléseket
+ Becsült kedvelések megjelenítése
Megjeleníti a becsült kedvelések számát a videóknál.
Köszöntő megjelenítése, ha az API nem elérhető
Megjelenik egy üzenet, ha a YouTube nem tetszések visszaállítása API nem érhető el.
@@ -372,6 +372,12 @@ Kattints ide az API-kulcs megszerzéséhez."
Letiltja a betöltési animációt amikor az app indul.
DRC hang letiltása
Letiltja a hangra alkalmazott DRC-t (dinamikatartomány-kompresszió).
+ Zenei videó letiltása az albumban
+ "Amikor egy nem prémium felhasználó lejátszik egy albumban szereplő dalt, néha a hivatalos dal helyett a klip kerül lejátszásra.
+
+Ha egy albumban szereplő zenei videó lejátszását észleli, a rendszer a hivatalos dalra irányítja át.
+
+Piped instance használatban van, és előfordulhat, hogy az API egyes régiókban nem elérhető."
Hibanaplók engedélyezése
Kiírja a hibanaplót.
Hibakeresési puffer naplózásának engedélyezése
@@ -399,28 +405,26 @@ A GmsCore akkumulátor-optimalizálás letiltása nem fogyasztja jobban az akkum
Nyomj a folytatás gombra, és engedélyezd az optimalizálási módosításokat."
Folytatás
Kliens hamisítása
- "A kliens hamisítása a lejátszási problémák elkerülése érdekében.
+ "Az kliens meghamisítása a lejátszási problémák megelőzése érdekében.
-※ Az 'Adatfolyam meghamisítása' használata esetén lejátszási problémák léphetnek fel."
+• Az 'Adatfolyam meghamisítása' funkcióval együtt használva lejátszási problémák léphetnek fel."
Alapértelmezett kliens
"Meghatározza az alapértelmezett klienst a hamisításhoz.
-※ Az Android kliens használata esetén ajánlott a 'Alkalmazás verziójának meghamisítása' használni."
+• Az Android kliens használata esetén ajánlott a 'Alkalmazás verziójának meghamisítása' használni."
Android Music 4.27.53
Android Music 5.29.53
iOS Music 6.21
Adatfolyam meghamisítása
"A lejátszási problémák megelőzése érdekében hamisítja a streaming-adatokat.
-※ A 'Kliens hamisítása' használata esetén lejátszási problémák léphetnek fel."
+• A 'Kliens hamisítása' használata esetén lejátszási problémák léphetnek fel."
Alapértelmezett kliens
Meghatároz egy alapértelmezett klienst, amely streaming adatokat hív le.
Megjelenik a statisztikák kockáknakban
Megmutatja a streaming adatok lekérdezésére használt klienst a Statisztikák kockáknakban.
- iOS
- iOS Music
- Android TV
Android VR
+ Android Music
Megosztási linkek tisztítása
Linkek megosztásakor eltávolítja a nyomkövetési paramétereket az URL-ekből.
Alapértelmezett program beállítások megnyitása
diff --git a/patches/src/main/resources/music/translations/ja-rJP/strings.xml b/patches/src/main/resources/music/translations/ja-rJP/strings.xml
index c1300fea2..26e984459 100644
--- a/patches/src/main/resources/music/translations/ja-rJP/strings.xml
+++ b/patches/src/main/resources/music/translations/ja-rJP/strings.xml
@@ -369,6 +369,16 @@ API キーの発行方法については、ここをタップしてください
Cairo スプラッシュアニメーションを無効にする
アプリ起動時のCairo のスプラッシュアニメーションを無効にします。
DRCオーディオを無効にする
+ 音声に適用されるDRC (Dynamic Range Compression) を無効にします。
+ アルバム内のミュージックビデオを無効にする
+ "非プレミアムユーザーがアルバムに含まれる曲を再生すると、公式の楽曲でなくMVが再生されることがあります。
+
+アルバムからMVの再生が検出された場合、公式の楽曲を検索します。
+
+• Piped Instance APIを利用しています。"
+ リダイレクトのタイプ
+ 公式楽曲にリダイレクトする方法を指定します。
+ リダイレクト
デバッグログ
デバッグログを出力します。
デバッグバッファログを有効化
@@ -392,9 +402,14 @@ API キーの発行方法については、ここをタップしてください
• OPUSオーディオコーデックはサポートされていない可能性があります。
• シークバーのサムネイルが表示されない場合があります。
• 再生履歴はブランドアカウントでは動作しません。"
- iOS
- Android TV
+ 既定のクライアント
+ Android Music 4.27.53
+ Android Music 5.29.53
+ iOS Music 6.21
+ ストリーミングデータを偽装
+ 既定のクライアント
Android VR
+ Android Music
共有リンクのクリーンアップ
リンクを共有する際に、URL からトラッキングクエリパラメーターを削除します。
「デフォルトで開く」の設定
diff --git a/patches/src/main/resources/music/translations/ko-rKR/strings.xml b/patches/src/main/resources/music/translations/ko-rKR/strings.xml
index 0ff9670d8..c54c2a638 100644
--- a/patches/src/main/resources/music/translations/ko-rKR/strings.xml
+++ b/patches/src/main/resources/music/translations/ko-rKR/strings.xml
@@ -220,8 +220,8 @@
동영상 플레이어의 색상을 회색조로 설정해 눈의 피로를 줄입니다.
팟캐스트에서 집중 모드 활성화
팟캐스트에서 집중 모드를 활성화합니다.
- \'노래↔동영상\' 전환 토글 제거
- 플레이어에서 \'노래↔동영상\' 전환 토글을 숨깁니다.
+ \'노래↔동영상\' 토글 제거
+ 플레이어에서 \'노래↔동영상\' 토글을 숨깁니다.
커뮤니티 가이드라인 제거
댓글 섹션 상단에서 커뮤니티 가이드라인을 숨깁니다.
타임스탬프, 이모지 버튼 제거
@@ -376,6 +376,17 @@ API Key를 발급받는 방법을 보려면 여기를 누르세요."
앱을 시작할 때, Cairo 스플래시 애니메이션을 비활성화합니다.
DRC 오디오 비활성화
오디오에 적용된 DRC (Dynamic Range Compression)를 비활성화합니다.
+ 앨범에서 뮤직 비디오 비활성화
+ "프리미엄이 아닌 사용자가 앨범에 포함된 노래를 재생할 때, 간혹 공식 음악 대신 뮤직 비디오가 재생되는 경우가 있습니다.
+
+앨범에서 뮤직 비디오가 재생되는 것이 감지되면 공식 음원을 찾아줍니다.
+
+• 파이핑된 인스턴스 API로 구동됩니다."
+ 리다이렉션 유형
+ 공식 음원으로 리다이렉션하는 방법을 지정할 수 있습니다.
+ 리다이렉션
+ \'노래↔동영상\' 토글 누르기
+ \'노래↔동영상\' 토글 길게 누르기
디버그 로깅 활성화
디버그 로그를 출력합니다.
디버그 버퍼 로깅 활성화
@@ -403,26 +414,24 @@ API Key를 발급받는 방법을 보려면 여기를 누르세요."
클라이언트 변경
"클라이언트를 변경하여 재생 문제를 방지할 수 있습니다.
-※ '스트리밍 데이터 변경'과 함께 사용할 경우에 재생 문제가 발생할 수 있습니다."
+• '스트리밍 데이터 변경'과 함께 사용할 경우에 재생 문제가 발생할 수 있습니다."
기본 클라이언트
"변경할 기본 클라이언트를 정의합니다.
-※ Android 클라이언트를 사용할 경우에 '앱 버전 변경'과 함께 사용하는 것을 권장합니다."
+• Android 클라이언트를 사용할 경우에 '앱 버전 변경'과 함께 사용하는 것을 권장합니다."
Android Music 4.27.53
Android Music 5.29.53
iOS Music 6.21
스트리밍 데이터 변경
"스트리밍 데이터를 변경하여 재생 문제를 방지합니다.
-※ '클라이언트 변경'과 함께 사용할 경우에 재생 문제가 발생할 수 있습니다."
+• '클라이언트 변경'과 함께 사용할 경우에 재생 문제가 발생할 수 있습니다."
기본 클라이언트
스트리밍 데이터를 가져오는 데 사용되는 기본 클라이언트를 정의할 수 있습니다.
동영상 통계에서 표시
\'스트리밍 데이터를 가져오는 데 사용되는 클라이언트\'가 동영상 통계에서 표시됩니다.
- iOS
- iOS Music
- Android TV
Android VR
+ Android Music
추적 쿼리를 제거한 링크 공유
링크를 공유할 때, URL에서 추적 쿼리 매개변수를 제거합니다.
기본 앱 설정 열기
diff --git a/patches/src/main/resources/music/translations/pl-rPL/strings.xml b/patches/src/main/resources/music/translations/pl-rPL/strings.xml
index b8b202749..7def1c10f 100644
--- a/patches/src/main/resources/music/translations/pl-rPL/strings.xml
+++ b/patches/src/main/resources/music/translations/pl-rPL/strings.xml
@@ -375,6 +375,17 @@ Kliknij, by zobaczyć, jak zgłosić klucz API."