Merge branch 'dev' into revanced-extended

This commit is contained in:
inotia00 2025-01-07 14:16:07 +09:00
commit 1aaf03df50
156 changed files with 5715 additions and 2857 deletions

View File

@ -15,7 +15,7 @@ See the [documentation](https://github.com/inotia00/revanced-documentation#readm
| `Ambient mode control` | Adds options to disable Ambient mode and to bypass Ambient mode restrictions. | 18.29.38 ~ 19.44.39 | | `Ambient mode control` | Adds options to disable Ambient mode and to bypass Ambient mode restrictions. | 18.29.38 ~ 19.44.39 |
| `Bypass image region restrictions` | Adds an option to use a different host for static images, so that images blocked in some countries can be received. | 18.29.38 ~ 19.44.39 | | `Bypass image region restrictions` | Adds an option to use a different host for static images, so that images blocked in some countries can be received. | 18.29.38 ~ 19.44.39 |
| `Change player flyout menu toggles` | Adds an option to use text toggles instead of switch toggles within the additional settings menu. | 18.29.38 ~ 19.44.39 | | `Change player flyout menu toggles` | Adds an option to use text toggles instead of switch toggles within the additional settings menu. | 18.29.38 ~ 19.44.39 |
| `Change share sheet` | Add option to change from in-app share sheet to system share sheet. | 18.29.38 ~ 19.44.39 | | `Change share sheet` | Adds an option to change the in-app share sheet to the system share sheet. | 18.29.38 ~ 19.44.39 |
| `Change start page` | Adds an option to set which page the app opens in instead of the homepage. | 18.29.38 ~ 19.44.39 | | `Change start page` | Adds an option to set which page the app opens in instead of the homepage. | 18.29.38 ~ 19.44.39 |
| `Custom Shorts action buttons` | Changes, at compile time, the icon of the action buttons of the Shorts player. | 18.29.38 ~ 19.44.39 | | `Custom Shorts action buttons` | Changes, at compile time, the icon of the action buttons of the Shorts player. | 18.29.38 ~ 19.44.39 |
| `Custom branding icon for YouTube` | Changes the YouTube app icon to the icon specified in patch options. | 18.29.38 ~ 19.44.39 | | `Custom branding icon for YouTube` | Changes the YouTube app icon to the icon specified in patch options. | 18.29.38 ~ 19.44.39 |
@ -46,7 +46,7 @@ See the [documentation](https://github.com/inotia00/revanced-documentation#readm
| `Hide layout components` | Adds options to hide general layout components. | 18.29.38 ~ 19.44.39 | | `Hide layout components` | Adds options to hide general layout components. | 18.29.38 ~ 19.44.39 |
| `Hide player buttons` | Adds options to hide buttons in the video player. | 18.29.38 ~ 19.44.39 | | `Hide player buttons` | Adds options to hide buttons in the video player. | 18.29.38 ~ 19.44.39 |
| `Hide player flyout menu` | Adds options to hide player flyout menu components. | 18.29.38 ~ 19.44.39 | | `Hide player flyout menu` | Adds options to hide player flyout menu components. | 18.29.38 ~ 19.44.39 |
| `Hide shortcuts` | Remove, at compile time, the app shortcuts that appears when app icon is long pressed. | 18.29.38 ~ 19.44.39 | | `Hide shortcuts` | Remove, at compile time, the app shortcuts that appears when the app icon is long pressed. | 18.29.38 ~ 19.44.39 |
| `Hook YouTube Music actions` | Adds support for opening music in RVX Music using the in-app YouTube Music button. | 18.29.38 ~ 19.44.39 | | `Hook YouTube Music actions` | Adds support for opening music in RVX Music using the in-app YouTube Music button. | 18.29.38 ~ 19.44.39 |
| `Hook download actions` | Adds support to download videos with an external downloader app using the in-app download button. | 18.29.38 ~ 19.44.39 | | `Hook download actions` | Adds support to download videos with an external downloader app using the in-app download button. | 18.29.38 ~ 19.44.39 |
| `Layout switch` | Adds an option to spoof the dpi in order to use a tablet or phone layout. | 18.29.38 ~ 19.44.39 | | `Layout switch` | Adds an option to spoof the dpi in order to use a tablet or phone layout. | 18.29.38 ~ 19.44.39 |
@ -68,7 +68,7 @@ See the [documentation](https://github.com/inotia00/revanced-documentation#readm
| `Spoof streaming data` | Adds options to spoof the streaming data to allow playback. | 18.29.38 ~ 19.44.39 | | `Spoof streaming data` | Adds options to spoof the streaming data to allow playback. | 18.29.38 ~ 19.44.39 |
| `Swipe controls` | Adds options for controlling volume and brightness with swiping, and whether to enter fullscreen when swiping down below the player. | 18.29.38 ~ 19.44.39 | | `Swipe controls` | Adds options for controlling volume and brightness with swiping, and whether to enter fullscreen when swiping down below the player. | 18.29.38 ~ 19.44.39 |
| `Theme` | Changes the app's theme to the values specified in patch options. | 18.29.38 ~ 19.44.39 | | `Theme` | Changes the app's theme to the values specified in patch options. | 18.29.38 ~ 19.44.39 |
| `Toolbar components` | Adds options to hide or change components located on the toolbar, such as toolbar buttons, search bar, and header. | 18.29.38 ~ 19.44.39 | | `Toolbar components` | Adds options to hide or change components located on the toolbar, such as the search bar, header, and toolbar buttons. | 18.29.38 ~ 19.44.39 |
| `Translations for YouTube` | Add translations or remove string resources. | 18.29.38 ~ 19.44.39 | | `Translations for YouTube` | Add translations or remove string resources. | 18.29.38 ~ 19.44.39 |
| `Video playback` | Adds options to customize settings related to video playback, such as default video quality and playback speed. | 18.29.38 ~ 19.44.39 | | `Video playback` | Adds options to customize settings related to video playback, such as default video quality and playback speed. | 18.29.38 ~ 19.44.39 |
| `Visual preferences icons for YouTube` | Adds icons to specific preferences in the settings. | 18.29.38 ~ 19.44.39 | | `Visual preferences icons for YouTube` | Adds icons to specific preferences in the settings. | 18.29.38 ~ 19.44.39 |
@ -80,19 +80,20 @@ See the [documentation](https://github.com/inotia00/revanced-documentation#readm
| 💊 Patch | 📜 Description | 🏹 Target Version | | 💊 Patch | 📜 Description | 🏹 Target Version |
|:--------:|:--------------:|:-----------------:| |:--------:|:--------------:|:-----------------:|
| `Amoled` | Applies a pure black theme to some components. | 6.20.51 ~ 7.25.53 |
| `Bitrate default value` | Sets the audio quality to 'Always High' when you first install the app. | 6.20.51 ~ 7.25.53 | | `Bitrate default value` | Sets the audio quality to 'Always High' when you first install the app. | 6.20.51 ~ 7.25.53 |
| `Bypass image region restrictions` | Adds an option to use a different host for static images, so that images blocked in some countries can be received. | 6.20.51 ~ 7.25.53 | | `Bypass image region restrictions` | Adds an option to use a different host for static images, so that images blocked in some countries can be received. | 6.20.51 ~ 7.25.53 |
| `Certificate spoof` | Enables YouTube Music to work with Android Auto by spoofing the YouTube Music certificate. | 6.20.51 ~ 7.25.53 | | `Certificate spoof` | Enables YouTube Music to work with Android Auto by spoofing the YouTube Music certificate. | 6.20.51 ~ 7.25.53 |
| `Change share sheet` | Add option to change from in-app share sheet to system share sheet. | 6.20.51 ~ 7.25.53 | | `Change share sheet` | Adds an option to change the in-app share sheet to the system share sheet. | 6.20.51 ~ 7.25.53 |
| `Change start page` | Adds an option to set which page the app opens in instead of the homepage. | 6.20.51 ~ 7.25.53 | | `Change start page` | Adds an option to set which page the app opens in instead of the homepage. | 6.20.51 ~ 7.25.53 |
| `Custom branding icon for YouTube Music` | Changes the YouTube Music app icon to the icon specified in patch options. | 6.20.51 ~ 7.25.53 | | `Custom branding icon for YouTube Music` | Changes the YouTube Music app icon to the icon specified in patch options. | 6.20.51 ~ 7.25.53 |
| `Custom branding name for YouTube Music` | Renames the YouTube Music app to the name specified in patch options. | 6.20.51 ~ 7.25.53 | | `Custom branding name for YouTube Music` | Renames the YouTube Music app to the name specified in patch options. | 6.20.51 ~ 7.25.53 |
| `Custom header for YouTube Music` | Applies a custom header in the top left corner within the app. | 6.20.51 ~ 7.25.53 | | `Custom header for YouTube Music` | Applies a custom header in the top left corner within the app. | 6.20.51 ~ 7.25.53 |
| `Dark theme` | Changes the app's dark theme to the values specified in patch options. | 6.20.51 ~ 7.25.53 |
| `Disable Cairo splash animation` | Adds an option to disable Cairo splash animation. | 7.06.54 ~ 7.25.53 | | `Disable Cairo splash animation` | Adds an option to disable Cairo splash animation. | 7.06.54 ~ 7.25.53 |
| `Disable DRC audio` | Adds an option to disable DRC (Dynamic Range Compression) audio. | 6.20.51 ~ 7.25.53 | | `Disable DRC audio` | Adds an option to disable DRC (Dynamic Range Compression) audio. | 6.20.51 ~ 7.25.53 |
| `Disable auto captions` | Adds an option to disable captions from being automatically enabled. | 6.20.51 ~ 7.25.53 | | `Disable auto captions` | Adds an option to disable captions from being automatically enabled. | 6.20.51 ~ 7.25.53 |
| `Disable dislike redirection` | Adds an option to disable redirection to the next track when clicking the Dislike button. | 6.20.51 ~ 7.25.53 | | `Disable dislike redirection` | Adds an option to disable redirection to the next track when clicking the Dislike button. | 6.20.51 ~ 7.25.53 |
| `Disable music video in album` | Adds option to redirect music videos from albums for non-premium users. | 6.20.51 ~ 7.25.53 |
| `Enable OPUS codec` | Adds an options to enable the OPUS audio codec if the player response includes. | 6.20.51 ~ 7.25.53 | | `Enable OPUS codec` | Adds an options to enable the OPUS audio codec if the player response includes. | 6.20.51 ~ 7.25.53 |
| `Enable debug logging` | Adds an option to enable debug logging. | 6.20.51 ~ 7.25.53 | | `Enable debug logging` | Adds an option to enable debug logging. | 6.20.51 ~ 7.25.53 |
| `Enable landscape mode` | Adds an option to enable landscape mode when rotating the screen on phones. | 6.20.51 ~ 7.25.53 | | `Enable landscape mode` | Adds an option to enable landscape mode when rotating the screen on phones. | 6.20.51 ~ 7.25.53 |

View File

@ -0,0 +1,186 @@
package app.revanced.extension.music.patches.misc;
import android.view.View;
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import app.revanced.extension.music.patches.misc.requests.PipedRequester;
import app.revanced.extension.music.settings.Settings;
import app.revanced.extension.music.shared.VideoInformation;
import app.revanced.extension.music.utils.VideoUtils;
import app.revanced.extension.shared.utils.Logger;
@SuppressWarnings("unused")
public class AlbumMusicVideoPatch {
public enum RedirectType {
REDIRECT,
ON_CLICK,
ON_LONG_CLICK
}
private static final RedirectType REDIRECT_TYPE =
Settings.DISABLE_MUSIC_VIDEO_IN_ALBUM_REDIRECT_TYPE.get();
private static final boolean DISABLE_MUSIC_VIDEO_IN_ALBUM =
Settings.DISABLE_MUSIC_VIDEO_IN_ALBUM.get();
private static final boolean REDIRECT = REDIRECT_TYPE == RedirectType.REDIRECT;
private static final boolean ON_CLICK = REDIRECT_TYPE == RedirectType.ON_CLICK;
private static final boolean ON_LONG_CLICK = REDIRECT_TYPE == RedirectType.ON_LONG_CLICK;
private static final String YOUTUBE_MUSIC_ALBUM_PREFIX = "OLAK";
private static final AtomicBoolean isVideoLaunched = new AtomicBoolean(false);
@NonNull
private static volatile String playerResponseVideoId = "";
@NonNull
private static volatile String currentVideoId = "";
@GuardedBy("itself")
private static final Map<String, String> lastVideoIds = new LinkedHashMap<>() {
private static final int NUMBER_OF_LAST_VIDEO_IDS_TO_TRACK = 10;
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > NUMBER_OF_LAST_VIDEO_IDS_TO_TRACK;
}
};
/**
* Injection point.
*/
public static void newPlayerResponse(@NonNull String videoId, @NonNull String playlistId, final int playlistIndex) {
if (!DISABLE_MUSIC_VIDEO_IN_ALBUM) {
return;
}
if (!playlistId.startsWith(YOUTUBE_MUSIC_ALBUM_PREFIX)) {
return;
}
if (playlistIndex < 0) {
return;
}
if (playerResponseVideoId.equals(videoId)) {
return;
}
playerResponseVideoId = videoId;
// Fetch Piped instance.
PipedRequester.fetchRequestIfNeeded(videoId, playlistId, playlistIndex);
}
/**
* Injection point.
*/
public static void newVideoLoaded(@NonNull String videoId) {
if (!DISABLE_MUSIC_VIDEO_IN_ALBUM) {
return;
}
if (currentVideoId.equals(videoId)) {
return;
}
currentVideoId = videoId;
checkVideo(videoId);
}
private static void checkVideo(@NonNull String videoId) {
try {
PipedRequester request = PipedRequester.getRequestForVideoId(videoId);
if (request == null) {
return;
}
String songId = request.getStream();
if (songId == null) {
return;
}
synchronized (lastVideoIds) {
if (lastVideoIds.put(videoId, songId) == null) {
Logger.printDebug(() -> "Official song found, videoId: " + videoId + ", songId: " + songId);
if (REDIRECT) {
openMusic(songId);
}
}
}
} catch (Exception ex) {
Logger.printException(() -> "check failure", ex);
}
}
/**
* Injection point.
*/
public static boolean openMusic() {
if (DISABLE_MUSIC_VIDEO_IN_ALBUM && ON_CLICK) {
try {
String videoId = VideoInformation.getVideoId();
synchronized (lastVideoIds) {
String songId = lastVideoIds.get(videoId);
if (songId != null) {
openMusic(songId);
return true;
}
}
} catch (Exception ex) {
Logger.printException(() -> "openMusic failure", ex);
}
}
return false;
}
private static void openMusic(@NonNull String songId) {
try {
isVideoLaunched.compareAndSet(false, true);
// The newly opened video is not a music video.
// To prevent fetch requests from being sent, set the video id to the newly opened video
VideoUtils.runOnMainThreadDelayed(() -> {
playerResponseVideoId = songId;
currentVideoId = songId;
VideoUtils.openInYouTubeMusic(songId);
}, 500);
VideoUtils.runOnMainThreadDelayed(() -> isVideoLaunched.compareAndSet(true, false), 1500);
} catch (Exception ex) {
Logger.printException(() -> "openMusic failure", ex);
}
}
/**
* Injection point.
*/
public static void setAudioVideoSwitchToggleOnLongClickListener(View view) {
if (DISABLE_MUSIC_VIDEO_IN_ALBUM && ON_LONG_CLICK) {
view.setOnLongClickListener(v -> {
try {
String videoId = VideoInformation.getVideoId();
synchronized (lastVideoIds) {
String songId = lastVideoIds.get(videoId);
if (songId != null) {
openMusic(songId);
}
}
} catch (Exception ex) {
Logger.printException(() -> "onLongClickListener failure", ex);
}
return true;
});
}
}
/**
* Injection point.
*/
public static boolean hideSnackBar() {
return DISABLE_MUSIC_VIDEO_IN_ALBUM && isVideoLaunched.get();
}
}

View File

@ -0,0 +1,177 @@
package app.revanced.extension.music.patches.misc.requests;
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.util.HashMap;
import java.util.Map;
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.requests.Requester;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.Utils;
public class PipedRequester {
/**
* 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<String, PipedRequester> cache = new HashMap<>();
@SuppressLint("ObsoleteSdkInt")
public static void fetchRequestIfNeeded(@NonNull String videoId, @NonNull String playlistId, final int playlistIndex) {
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)) {
PipedRequester pipedRequester = new PipedRequester(videoId, playlistId, playlistIndex);
cache.put(videoId, pipedRequester);
}
}
}
@Nullable
public static PipedRequester getRequestForVideoId(@Nullable String videoId) {
synchronized (cache) {
return cache.get(videoId);
}
}
/**
* TCP timeout
*/
private static final int TIMEOUT_TCP_DEFAULT_MILLISECONDS = 2 * 1000; // 2 seconds
/**
* HTTP response timeout
*/
private static final int TIMEOUT_HTTP_DEFAULT_MILLISECONDS = 4 * 1000; // 4 seconds
@Nullable
private static JSONObject send(@NonNull String videoId, @NonNull String playlistId, final int playlistIndex) {
final long startTime = System.currentTimeMillis();
Logger.printDebug(() -> "Fetching piped instances (videoId: '" + videoId +
"', playlistId: '" + playlistId + "', playlistIndex: '" + playlistIndex + "'");
try {
HttpURLConnection connection = PipedRoutes.getPlaylistConnectionFromRoute(playlistId);
connection.setConnectTimeout(TIMEOUT_TCP_DEFAULT_MILLISECONDS);
connection.setReadTimeout(TIMEOUT_HTTP_DEFAULT_MILLISECONDS);
final int responseCode = connection.getResponseCode();
if (responseCode == 200) return Requester.parseJSONObject(connection);
handleConnectionError("API not available: " + responseCode);
} 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(() -> "playlist: " + playlistId + " took: " + (System.currentTimeMillis() - startTime) + "ms");
}
return null;
}
@Nullable
private static String fetch(@NonNull String videoId, @NonNull String playlistId, final int playlistIndex) {
final JSONObject playlistJson = send(videoId, playlistId, playlistIndex);
if (playlistJson != null) {
try {
final String songId = playlistJson.getJSONArray("relatedStreams")
.getJSONObject(playlistIndex)
.getString("url")
.replaceAll("/.+=", "");
if (songId.isEmpty()) {
handleConnectionError("Url is empty!");
} else if (!songId.equals(videoId)) {
return songId;
}
} catch (JSONException e) {
Logger.printDebug(() -> "Fetch failed while processing response data for response: " + playlistJson);
}
}
return null;
}
private static void handleConnectionError(@NonNull String errorMessage) {
handleConnectionError(errorMessage, null);
}
private static void handleConnectionError(@NonNull String errorMessage, @Nullable Exception ex) {
if (ex != null) {
Logger.printInfo(() -> errorMessage, ex);
}
}
/**
* Time this instance and the fetch future was created.
*/
private final long timeFetched;
private final String videoId;
private final Future<String> future;
private PipedRequester(@NonNull String videoId, @NonNull String playlistId, final int playlistIndex) {
this.timeFetched = System.currentTimeMillis();
this.videoId = videoId;
this.future = Utils.submitOnBackgroundThread(() -> fetch(videoId, playlistId, playlistIndex));
}
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 String 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;
}
}

View File

@ -0,0 +1,22 @@
package app.revanced.extension.music.patches.misc.requests;
import static app.revanced.extension.shared.requests.Route.Method.GET;
import java.io.IOException;
import java.net.HttpURLConnection;
import app.revanced.extension.shared.requests.Requester;
import app.revanced.extension.shared.requests.Route;
class PipedRoutes {
private static final String PIPED_URL = "https://pipedapi.kavin.rocks/";
private static final Route GET_PLAYLIST = new Route(GET, "playlists/{playlist_id}");
private PipedRoutes() {
}
static HttpURLConnection getPlaylistConnectionFromRoute(String... params) throws IOException {
return Requester.getConnectionFromRoute(PIPED_URL, GET_PLAYLIST, params);
}
}

View File

@ -8,13 +8,16 @@ import android.widget.TextView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import app.revanced.extension.music.patches.utils.PatchStatus;
import app.revanced.extension.music.settings.Settings; import app.revanced.extension.music.settings.Settings;
import app.revanced.extension.shared.utils.ResourceUtils; import app.revanced.extension.shared.utils.ResourceUtils;
@SuppressWarnings("unused") @SuppressWarnings("unused")
public class NavigationPatch { public class NavigationPatch {
private static final int colorGrey12 = private static final int colorGrey12 = PatchStatus.DarkTheme()
ResourceUtils.getColor("revanced_color_grey_12"); ? ResourceUtils.getColor("ytm_color_grey_12")
: ResourceUtils.getColor("revanced_color_grey_12");
public static Enum<?> lastPivotTab; public static Enum<?> lastPivotTab;
public static int enableBlackNavigationBar() { public static int enableBlackNavigationBar() {

View File

@ -1,21 +1,50 @@
package app.revanced.extension.music.patches.utils; package app.revanced.extension.music.patches.utils;
import android.graphics.drawable.Drawable;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ImageView;
import org.apache.commons.lang3.ArrayUtils;
import app.revanced.extension.shared.utils.ResourceUtils;
@SuppressWarnings("unused") @SuppressWarnings("unused")
public class DrawableColorPatch { public class DrawableColorPatch {
private static final int[] DARK_VALUES = { private static final int[] DARK_VALUES = {
-14606047 // comments box background -14606047, // comments box background
-16579837, // button container background in album
-16777216, // button container background in playlist
}; };
// background colors
private static final Drawable headerGradient =
ResourceUtils.getDrawable("revanced_header_gradient");
private static final int blackColor =
ResourceUtils.getColor("yt_black1");
private static final int elementsContainerIdentifier =
ResourceUtils.getIdIdentifier("elements_container");
public static int getLithoColor(int originalValue) { public static int getLithoColor(int originalValue) {
if (anyEquals(originalValue, DARK_VALUES)) return ArrayUtils.contains(DARK_VALUES, originalValue)
return -16777215; ? blackColor
: originalValue;
return originalValue;
} }
private static boolean anyEquals(int value, int... of) { public static void setHeaderGradient(ViewGroup viewGroup) {
for (int v : of) if (value == v) return true; viewGroup.getViewTreeObserver().addOnGlobalLayoutListener(() -> {
return false; if (!(viewGroup instanceof FrameLayout frameLayout))
return;
if (!(frameLayout.getChildAt(0) instanceof ViewGroup parentViewGroup))
return;
if (!(parentViewGroup.getChildAt(0) instanceof ImageView gradientView))
return;
// For some reason, it sometimes applies to other lithoViews.
// To prevent this, check the viewId before applying the gradient.
if (headerGradient != null && viewGroup.getId() == elementsContainerIdentifier) {
gradientView.setForeground(headerGradient);
}
});
} }
} }

View File

@ -2,6 +2,11 @@ package app.revanced.extension.music.patches.utils;
@SuppressWarnings("unused") @SuppressWarnings("unused")
public class PatchStatus { public class PatchStatus {
public static boolean DarkTheme() {
// Replace this with true if the Dark theme patch succeeds
return false;
}
public static boolean SpoofAppVersionDefaultBoolean() { public static boolean SpoofAppVersionDefaultBoolean() {
return false; return false;
} }

View File

@ -6,6 +6,7 @@ import static app.revanced.extension.music.sponsorblock.objects.CategoryBehaviou
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import app.revanced.extension.music.patches.misc.AlbumMusicVideoPatch.RedirectType;
import app.revanced.extension.music.patches.misc.client.AppClient.ClientType; import app.revanced.extension.music.patches.misc.client.AppClient.ClientType;
import app.revanced.extension.music.patches.utils.PatchStatus; import app.revanced.extension.music.patches.utils.PatchStatus;
import app.revanced.extension.music.sponsorblock.SponsorBlockSettings; import app.revanced.extension.music.sponsorblock.SponsorBlockSettings;
@ -116,7 +117,7 @@ public class Settings extends BaseSettings {
// PreferenceScreen: Navigation bar // PreferenceScreen: Navigation bar
public static final BooleanSetting ENABLE_BLACK_NAVIGATION_BAR = new BooleanSetting("revanced_enable_black_navigation_bar", TRUE); public static final BooleanSetting ENABLE_BLACK_NAVIGATION_BAR = new BooleanSetting("revanced_enable_black_navigation_bar", FALSE);
public static final BooleanSetting HIDE_NAVIGATION_HOME_BUTTON = new BooleanSetting("revanced_hide_navigation_home_button", FALSE, true); public static final BooleanSetting HIDE_NAVIGATION_HOME_BUTTON = new BooleanSetting("revanced_hide_navigation_home_button", FALSE, true);
public static final BooleanSetting HIDE_NAVIGATION_SAMPLES_BUTTON = new BooleanSetting("revanced_hide_navigation_samples_button", FALSE, true); public static final BooleanSetting HIDE_NAVIGATION_SAMPLES_BUTTON = new BooleanSetting("revanced_hide_navigation_samples_button", FALSE, true);
public static final BooleanSetting HIDE_NAVIGATION_EXPLORE_BUTTON = new BooleanSetting("revanced_hide_navigation_explore_button", FALSE, true); public static final BooleanSetting HIDE_NAVIGATION_EXPLORE_BUTTON = new BooleanSetting("revanced_hide_navigation_explore_button", FALSE, true);
@ -178,6 +179,8 @@ public class Settings extends BaseSettings {
public static final BooleanSetting CHANGE_SHARE_SHEET = new BooleanSetting("revanced_change_share_sheet", FALSE, true); public static final BooleanSetting CHANGE_SHARE_SHEET = new BooleanSetting("revanced_change_share_sheet", FALSE, true);
public static final BooleanSetting DISABLE_CAIRO_SPLASH_ANIMATION = new BooleanSetting("revanced_disable_cairo_splash_animation", FALSE, true); public static final BooleanSetting DISABLE_CAIRO_SPLASH_ANIMATION = new BooleanSetting("revanced_disable_cairo_splash_animation", FALSE, true);
public static final BooleanSetting DISABLE_DRC_AUDIO = new BooleanSetting("revanced_disable_drc_audio", FALSE, true); public static final BooleanSetting DISABLE_DRC_AUDIO = new BooleanSetting("revanced_disable_drc_audio", FALSE, true);
public static final BooleanSetting DISABLE_MUSIC_VIDEO_IN_ALBUM = new BooleanSetting("revanced_disable_music_video_in_album", FALSE, true);
public static final EnumSetting<RedirectType> DISABLE_MUSIC_VIDEO_IN_ALBUM_REDIRECT_TYPE = new EnumSetting<>("revanced_disable_music_video_in_album_redirect_type", RedirectType.REDIRECT, true);
public static final BooleanSetting ENABLE_OPUS_CODEC = new BooleanSetting("revanced_enable_opus_codec", FALSE, true); public static final BooleanSetting ENABLE_OPUS_CODEC = new BooleanSetting("revanced_enable_opus_codec", FALSE, true);
public static final BooleanSetting SETTINGS_IMPORT_EXPORT = new BooleanSetting("revanced_extended_settings_import_export", FALSE, false); public static final BooleanSetting SETTINGS_IMPORT_EXPORT = new BooleanSetting("revanced_extended_settings_import_export", FALSE, false);
public static final BooleanSetting SPOOF_CLIENT = new BooleanSetting("revanced_spoof_client", FALSE, true); public static final BooleanSetting SPOOF_CLIENT = new BooleanSetting("revanced_spoof_client", FALSE, true);
@ -247,6 +250,7 @@ public class Settings extends BaseSettings {
CHANGE_START_PAGE.key, CHANGE_START_PAGE.key,
CUSTOM_FILTER_STRINGS.key, CUSTOM_FILTER_STRINGS.key,
CUSTOM_PLAYBACK_SPEEDS.key, CUSTOM_PLAYBACK_SPEEDS.key,
DISABLE_MUSIC_VIDEO_IN_ALBUM_REDIRECT_TYPE.key,
EXTERNAL_DOWNLOADER_PACKAGE_NAME.key, EXTERNAL_DOWNLOADER_PACKAGE_NAME.key,
HIDE_ACCOUNT_MENU_FILTER_STRINGS.key, HIDE_ACCOUNT_MENU_FILTER_STRINGS.key,
SB_API_URL.key, SB_API_URL.key,

View File

@ -4,6 +4,7 @@ import static app.revanced.extension.music.settings.Settings.BYPASS_IMAGE_REGION
import static app.revanced.extension.music.settings.Settings.CHANGE_START_PAGE; import static app.revanced.extension.music.settings.Settings.CHANGE_START_PAGE;
import static app.revanced.extension.music.settings.Settings.CUSTOM_FILTER_STRINGS; import static app.revanced.extension.music.settings.Settings.CUSTOM_FILTER_STRINGS;
import static app.revanced.extension.music.settings.Settings.CUSTOM_PLAYBACK_SPEEDS; import static app.revanced.extension.music.settings.Settings.CUSTOM_PLAYBACK_SPEEDS;
import static app.revanced.extension.music.settings.Settings.DISABLE_MUSIC_VIDEO_IN_ALBUM_REDIRECT_TYPE;
import static app.revanced.extension.music.settings.Settings.EXTERNAL_DOWNLOADER_PACKAGE_NAME; import static app.revanced.extension.music.settings.Settings.EXTERNAL_DOWNLOADER_PACKAGE_NAME;
import static app.revanced.extension.music.settings.Settings.HIDE_ACCOUNT_MENU_FILTER_STRINGS; import static app.revanced.extension.music.settings.Settings.HIDE_ACCOUNT_MENU_FILTER_STRINGS;
import static app.revanced.extension.music.settings.Settings.OPEN_DEFAULT_APP_SETTINGS; import static app.revanced.extension.music.settings.Settings.OPEN_DEFAULT_APP_SETTINGS;
@ -160,9 +161,11 @@ public class ReVancedPreferenceFragment extends PreferenceFragment {
Logger.printDebug(() -> "Failed to find the right value: " + dataString); Logger.printDebug(() -> "Failed to find the right value: " + dataString);
} }
} else if (settings instanceof EnumSetting<?> enumSetting) { } else if (settings instanceof EnumSetting<?> enumSetting) {
if (settings.equals(RETURN_YOUTUBE_USERNAME_DISPLAY_FORMAT) if (settings.equals(DISABLE_MUSIC_VIDEO_IN_ALBUM_REDIRECT_TYPE)
|| settings.equals(RETURN_YOUTUBE_USERNAME_DISPLAY_FORMAT)
|| settings.equals(SPOOF_CLIENT_TYPE) || settings.equals(SPOOF_CLIENT_TYPE)
|| settings.equals(SPOOF_STREAMING_DATA_TYPE)) { || settings.equals(SPOOF_STREAMING_DATA_TYPE)
) {
ResettableListPreference.showDialog(mActivity, enumSetting, 0); ResettableListPreference.showDialog(mActivity, enumSetting, 0);
} }
} }

View File

@ -8,7 +8,10 @@ import android.app.Activity;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import org.apache.commons.lang3.ArrayUtils;
import java.util.Arrays; import java.util.Arrays;
import java.util.Locale;
import app.revanced.extension.shared.settings.EnumSetting; import app.revanced.extension.shared.settings.EnumSetting;
import app.revanced.extension.shared.settings.Setting; import app.revanced.extension.shared.settings.Setting;
@ -57,7 +60,7 @@ public class ResettableListPreference {
final String[] mEntries = getStringArray(entryKey); final String[] mEntries = getStringArray(entryKey);
final String[] mEntryValues = getStringArray(entryValueKey); final String[] mEntryValues = getStringArray(entryValueKey);
final int findIndex = Arrays.binarySearch(mEntryValues, setting.get().toString()); final int findIndex = ArrayUtils.indexOf(mEntryValues, setting.get().toString().toUpperCase(Locale.ENGLISH));
mClickedDialogEntryIndex = findIndex >= 0 ? findIndex : defaultIndex; mClickedDialogEntryIndex = findIndex >= 0 ? findIndex : defaultIndex;
getDialogBuilder(mActivity) getDialogBuilder(mActivity)

View File

@ -18,7 +18,7 @@ enum class PlayerType {
companion object { companion object {
private val nameToPlayerType = values().associateBy { it.name } private val nameToPlayerType = entries.associateBy { it.name }
@JvmStatic @JvmStatic
fun setFromString(enumName: String) { fun setFromString(enumName: String) {

View File

@ -19,7 +19,7 @@ enum class VideoType {
companion object { companion object {
private val nameToVideoType = values().associateBy { it.name } private val nameToVideoType = entries.associateBy { it.name }
@JvmStatic @JvmStatic
fun setFromString(enumName: String) { fun setFromString(enumName: String) {

View File

@ -71,6 +71,13 @@ public class VideoUtils extends IntentUtils {
launchView(url, context.getPackageName()); launchView(url, context.getPackageName());
} }
/**
* Rest of the implementation added by patch.
*/
public static void dismissQueue() {
Log.d("Extended: VideoUtils", "Queue dismissed");
}
/** /**
* Rest of the implementation added by patch. * Rest of the implementation added by patch.
*/ */

View File

@ -1,5 +1,6 @@
package app.revanced.extension.shared.patches; package app.revanced.extension.shared.patches;
import android.annotation.SuppressLint;
import android.util.Log; import android.util.Log;
import androidx.preference.PreferenceScreen; import androidx.preference.PreferenceScreen;
@ -10,6 +11,7 @@ public class BaseSettingsMenuPatch {
/** /**
* Rest of the implementation added by patch. * Rest of the implementation added by patch.
*/ */
@SuppressLint("LongLogTag")
public static void removePreference(PreferenceScreen mPreferenceScreen, String key) { public static void removePreference(PreferenceScreen mPreferenceScreen, String key) {
Log.d("Extended: SettingsMenuPatch", "key: " + key); Log.d("Extended: SettingsMenuPatch", "key: " + key);
} }

View File

@ -120,7 +120,11 @@ public class GmsCoreSupport {
} }
// Check if GmsCore is whitelisted from battery optimizations. // Check if GmsCore is whitelisted from battery optimizations.
if (batteryOptimizationsEnabled(mActivity)) { if (isAndroidAutomotive(mActivity)) {
// Ignore Android Automotive devices (Google built-in),
// as there is no way to disable battery optimizations.
Logger.printDebug(() -> "Device is Android Automotive");
} else if (batteryOptimizationsEnabled(mActivity)) {
Logger.printInfo(() -> "GmsCore is not whitelisted from battery optimizations"); Logger.printInfo(() -> "GmsCore is not whitelisted from battery optimizations");
showBatteryOptimizationDialog(mActivity, showBatteryOptimizationDialog(mActivity,
"gms_core_dialog_not_whitelisted_using_battery_optimizations_message", "gms_core_dialog_not_whitelisted_using_battery_optimizations_message",
@ -220,6 +224,10 @@ public class GmsCoreSupport {
return packageName; return packageName;
} }
private static boolean isAndroidAutomotive(Context context) {
return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE);
}
private static String getGmsCoreDownload() { private static String getGmsCoreDownload() {
final String vendorGroupId = getGmsCoreVendorGroupId(); final String vendorGroupId = getGmsCoreVendorGroupId();
return switch (vendorGroupId) { return switch (vendorGroupId) {

View File

@ -6,13 +6,8 @@ public class PatchStatus {
return false; return false;
} }
public static boolean SpoofStreamingData() { public static boolean SpoofStreamingDataMusic() {
// Replace this with true If the Spoof streaming data patch succeeds // Replace this with true If the Spoof streaming data patch succeeds in YouTube Music
return false;
}
public static boolean SpoofStreamingDataAndroidOnlyDefaultBoolean() {
// Replace this with true If the Spoof streaming data patch succeeds in YouTube
return false; return false;
} }
} }

View File

@ -1,313 +0,0 @@
package app.revanced.extension.shared.patches.client;
import static app.revanced.extension.shared.utils.ResourceUtils.getString;
import android.os.Build;
import androidx.annotation.Nullable;
import app.revanced.extension.shared.settings.BaseSettings;
public class AppClient {
// IOS
/**
* Video not playable: Paid / Movie / Private / Age-restricted
* Note: Audio track available
*/
private static final String PACKAGE_NAME_IOS = "com.google.ios.youtube";
/**
* The hardcoded client version of the iOS app used for InnerTube requests with this client.
*
* <p>
* It can be extracted by getting the latest release version of the app on
* <a href="https://apps.apple.com/us/app/youtube-watch-listen-stream/id544007664/">the App
* Store page of the YouTube app</a>, in the {@code Whats New} section.
* </p>
*/
private static final String CLIENT_VERSION_IOS = "19.29.1";
/**
* The device machine id for the iPhone 15 Pro Max (iPhone16,2), used to get HDR with AV1 hardware decoding.
*
* <p>
* See <a href="https://gist.github.com/adamawolf/3048717">this GitHub Gist</a> for more
* information.
* </p>
*/
private static final String DEVICE_MODEL_IOS = "iPhone16,2";
private static final String OS_VERSION_IOS = "17.7.2.21H221";
private static final String USER_AGENT_VERSION_IOS = "17_7_2";
private static final String USER_AGENT_IOS =
iOSUserAgent(PACKAGE_NAME_IOS, CLIENT_VERSION_IOS);
// IOS UNPLUGGED
/**
* Video not playable: Paid / Movie
* Note: Audio track available
*/
private static final String PACKAGE_NAME_IOS_UNPLUGGED = "com.google.ios.youtubeunplugged";
/**
* The hardcoded client version of the iOS app used for InnerTube requests with this client.
*
* <p>
* It can be extracted by getting the latest release version of the app on
* <a href="https://apps.apple.com/us/app/youtube-tv/id1193350206/">the App
* Store page of the YouTube TV app</a>, in the {@code Whats New} section.
* </p>
*/
private static final String CLIENT_VERSION_IOS_UNPLUGGED = "8.33";
private static final String USER_AGENT_IOS_UNPLUGGED =
iOSUserAgent(PACKAGE_NAME_IOS_UNPLUGGED, CLIENT_VERSION_IOS_UNPLUGGED);
// IOS MUSIC
/**
* Video not playable: All videos that can't be played on YouTube Music
*/
private static final String PACKAGE_NAME_IOS_MUSIC = "com.google.ios.youtubemusic";
/**
* The hardcoded client version of the iOS app used for InnerTube requests with this client.
*
* <p>
* It can be extracted by getting the latest release version of the app on
* <a href="https://apps.apple.com/us/app/youtube-music/id1017492454/">the App
* Store page of the YouTube Music app</a>, in the {@code Whats New} section.
* </p>
*/
private static final String CLIENT_VERSION_IOS_MUSIC = "7.04";
private static final String USER_AGENT_IOS_MUSIC =
iOSUserAgent(PACKAGE_NAME_IOS_MUSIC, CLIENT_VERSION_IOS_MUSIC);
// ANDROID VR
/**
* Video not playable: Kids
* Note: Audio track is not available
* <p>
* Package name for YouTube VR (Google DayDream): com.google.android.apps.youtube.vr (Deprecated)
* Package name for YouTube VR (Meta Quests): com.google.android.apps.youtube.vr.oculus
* Package name for YouTube VR (ByteDance Pico 4): com.google.android.apps.youtube.vr.pico
*/
private static final String PACKAGE_NAME_ANDROID_VR = "com.google.android.apps.youtube.vr.oculus";
/**
* The hardcoded client version of the Android VR app used for InnerTube requests with this client.
*
* <p>
* It can be extracted by getting the latest release version of the app on
* <a href="https://www.meta.com/en-us/experiences/2002317119880945/">the App
* Store page of the YouTube app</a>, in the {@code Additional details} section.
* </p>
*/
private static final String CLIENT_VERSION_ANDROID_VR = "1.61.48";
/**
* The device machine id for the Meta Quest 3, used to get opus codec with the Android VR client.
*
* <p>
* See <a href="https://dumps.tadiphone.dev/dumps/oculus/eureka">this GitLab</a> for more
* information.
* </p>
*/
private static final String DEVICE_MODEL_ANDROID_VR = "Quest 3";
private static final String OS_VERSION_ANDROID_VR = "12";
/**
* The SDK version for Android 12 is 31,
* but for some reason the build.props for the {@code Quest 3} state that the SDK version is 32.
*/
private static final String ANDROID_SDK_VERSION_ANDROID_VR = "32";
private static final String USER_AGENT_ANDROID_VR =
androidUserAgent(PACKAGE_NAME_ANDROID_VR, CLIENT_VERSION_ANDROID_VR, OS_VERSION_ANDROID_VR);
// ANDROID UNPLUGGED
/**
* Video not playable: Playlists / Music
* Note: Audio track is not available
*/
private static final String PACKAGE_NAME_ANDROID_UNPLUGGED = "com.google.android.apps.youtube.unplugged";
private static final String CLIENT_VERSION_ANDROID_UNPLUGGED = "8.16.0";
/**
* The device machine id for the Chromecast with Google TV 4K.
*
* <p>
* See <a href="https://dumps.tadiphone.dev/dumps/google/kirkwood">this GitLab</a> for more
* information.
* </p>
*/
private static final String DEVICE_MODEL_ANDROID_UNPLUGGED = "Google TV Streamer";
private static final String OS_VERSION_ANDROID_UNPLUGGED = "14";
private static final String ANDROID_SDK_VERSION_ANDROID_UNPLUGGED = "34";
private static final String USER_AGENT_ANDROID_UNPLUGGED =
androidUserAgent(PACKAGE_NAME_ANDROID_UNPLUGGED, CLIENT_VERSION_ANDROID_UNPLUGGED, OS_VERSION_ANDROID_UNPLUGGED);
// ANDROID CREATOR
/**
* Video not playable: Livestream
* Note: Audio track is not available
*/
private static final String PACKAGE_NAME_ANDROID_CREATOR = "com.google.android.apps.youtube.creator";
private static final String CLIENT_VERSION_ANDROID_CREATOR = "24.14.101";
private static final String DEVICE_MODEL_ANDROID_CREATOR = Build.MODEL;
private static final String OS_VERSION_ANDROID_CREATOR = Build.VERSION.RELEASE;
private static final String ANDROID_SDK_VERSION_ANDROID_CREATOR = String.valueOf(Build.VERSION.SDK_INT);
private static final String USER_AGENT_ANDROID_CREATOR =
androidUserAgent(PACKAGE_NAME_ANDROID_CREATOR, CLIENT_VERSION_ANDROID_CREATOR, OS_VERSION_ANDROID_CREATOR);
private AppClient() {
}
private static String androidUserAgent(String packageName, String clientVersion, String osVersion) {
return packageName +
"/" +
clientVersion +
" (Linux; U; Android " +
osVersion +
"; GB) gzip";
}
private static String iOSUserAgent(String packageName, String clientVersion) {
return packageName +
"/" +
clientVersion +
"(" +
DEVICE_MODEL_IOS +
"; U; CPU iOS " +
USER_AGENT_VERSION_IOS +
" like Mac OS X)";
}
public enum ClientType {
IOS(5,
DEVICE_MODEL_IOS,
OS_VERSION_IOS,
USER_AGENT_IOS,
null,
CLIENT_VERSION_IOS,
false
),
ANDROID_VR(28,
DEVICE_MODEL_ANDROID_VR,
OS_VERSION_ANDROID_VR,
USER_AGENT_ANDROID_VR,
ANDROID_SDK_VERSION_ANDROID_VR,
CLIENT_VERSION_ANDROID_VR,
true
),
ANDROID_UNPLUGGED(29,
DEVICE_MODEL_ANDROID_UNPLUGGED,
OS_VERSION_ANDROID_UNPLUGGED,
USER_AGENT_ANDROID_UNPLUGGED,
ANDROID_SDK_VERSION_ANDROID_UNPLUGGED,
CLIENT_VERSION_ANDROID_UNPLUGGED,
true
),
ANDROID_CREATOR(14,
DEVICE_MODEL_ANDROID_CREATOR,
OS_VERSION_ANDROID_CREATOR,
USER_AGENT_ANDROID_CREATOR,
ANDROID_SDK_VERSION_ANDROID_CREATOR,
CLIENT_VERSION_ANDROID_CREATOR,
true
),
IOS_UNPLUGGED(33,
DEVICE_MODEL_IOS,
OS_VERSION_IOS,
USER_AGENT_IOS_UNPLUGGED,
null,
CLIENT_VERSION_IOS_UNPLUGGED,
true
),
IOS_MUSIC(
26,
DEVICE_MODEL_IOS,
OS_VERSION_IOS,
USER_AGENT_IOS_MUSIC,
null,
CLIENT_VERSION_IOS_MUSIC,
true
);
/**
* YouTube
* <a href="https://github.com/zerodytrash/YouTube-Internal-Clients?tab=readme-ov-file#clients">client type</a>
*/
public final int id;
public final String clientName;
/**
* Device model, equivalent to {@link Build#MODEL} (System property: ro.product.model)
*/
public final String deviceModel;
/**
* Device OS version.
*/
public final String osVersion;
/**
* Player user-agent.
*/
public final String userAgent;
/**
* Android SDK version, equivalent to {@link Build.VERSION#SDK} (System property: ro.build.version.sdk)
* Field is null if not applicable.
*/
@Nullable
public final String androidSdkVersion;
/**
* App version.
*/
public final String clientVersion;
/**
* If the client can access the API logged in.
*/
public final boolean canLogin;
ClientType(int id,
String deviceModel,
String osVersion,
String userAgent,
@Nullable String androidSdkVersion,
String clientVersion,
boolean canLogin
) {
this.id = id;
this.clientName = name();
this.deviceModel = deviceModel;
this.clientVersion = clientVersion;
this.osVersion = osVersion;
this.androidSdkVersion = androidSdkVersion;
this.userAgent = userAgent;
this.canLogin = canLogin;
}
private static final ClientType[] CLIENT_ORDER_TO_USE_ANDROID = {
ANDROID_VR,
ANDROID_UNPLUGGED,
ANDROID_CREATOR,
};
private static final ClientType[] CLIENT_ORDER_TO_USE_DEFAULT = {
IOS,
ANDROID_VR,
ANDROID_UNPLUGGED,
IOS_UNPLUGGED,
IOS_MUSIC,
};
public final String getFriendlyName() {
return getString("revanced_spoof_streaming_data_type_entry_" + name().toLowerCase());
}
}
public static ClientType[] getAvailableClientTypes() {
return BaseSettings.SPOOF_STREAMING_DATA_ANDROID_ONLY.get()
? ClientType.CLIENT_ORDER_TO_USE_ANDROID
: ClientType.CLIENT_ORDER_TO_USE_DEFAULT;
}
}

View File

@ -0,0 +1,351 @@
package app.revanced.extension.shared.patches.client
import android.os.Build
import app.revanced.extension.shared.patches.PatchStatus
import app.revanced.extension.shared.settings.BaseSettings
import org.apache.commons.lang3.ArrayUtils
/**
* Used to fetch streaming data.
*/
object AppClient {
// IOS
/**
* Video not playable: Paid / Movie / Private / Age-restricted
* Note: Audio track available
*/
private const val PACKAGE_NAME_IOS = "com.google.ios.youtube"
/**
* The hardcoded client version of the iOS app used for InnerTube requests with this client.
*
* It can be extracted by getting the latest release version of the app on
* [the App Store page of the YouTube app](https://apps.apple.com/us/app/youtube-watch-listen-stream/id544007664/),
* in the `Whats New` section.
*/
private val CLIENT_VERSION_IOS = if (forceAVC())
"17.40.5"
else
"19.29.1"
private const val DEVICE_MAKE_IOS = "Apple"
private const val OS_NAME_IOS = "iOS"
/**
* The device machine id for the iPhone 15 Pro Max (iPhone16,2),
* used to get HDR with AV1 hardware decoding.
* See [this GitHub Gist](https://gist.github.com/adamawolf/3048717) for more information.
*/
private val DEVICE_MODEL_IOS = if (forceAVC())
"iPhone12,5" // 11 Pro Max. (last device with iOS 13)
else
"iPhone16,2" // 15 Pro Max.
private val OS_VERSION_IOS = if (forceAVC())
"13.7.17H35" // Last release of iOS 13.
else
"17.7.2.21H221"
private val USER_AGENT_VERSION_IOS = if (forceAVC())
"13_7"
else
"17_7_2"
private val USER_AGENT_IOS = iOSUserAgent(PACKAGE_NAME_IOS, CLIENT_VERSION_IOS)
// IOS UNPLUGGED
/**
* Video not playable: Paid / Movie / Playlists / Music
* Note: Audio track available
*/
private const val PACKAGE_NAME_IOS_UNPLUGGED = "com.google.ios.youtubeunplugged"
/**
* The hardcoded client version of the iOS app used for InnerTube requests with this client.
*
* It can be extracted by getting the latest release version of the app on
* [the App Store page of the YouTube TV app](https://apps.apple.com/us/app/youtube-tv/id1193350206/),
* in the `Whats New` section.
*/
private val CLIENT_VERSION_IOS_UNPLUGGED = if (forceAVC())
"6.45"
else
"8.33"
private val USER_AGENT_IOS_UNPLUGGED =
iOSUserAgent(PACKAGE_NAME_IOS_UNPLUGGED, CLIENT_VERSION_IOS_UNPLUGGED)
// ANDROID VR
/**
* Video not playable: Kids
* Note: Audio track is not available
*
* Package name for YouTube VR (Google DayDream): com.google.android.apps.youtube.vr (Deprecated)
* Package name for YouTube VR (Meta Quests): com.google.android.apps.youtube.vr.oculus
* Package name for YouTube VR (ByteDance Pico 4): com.google.android.apps.youtube.vr.pico
*/
private const val PACKAGE_NAME_ANDROID_VR = "com.google.android.apps.youtube.vr.oculus"
/**
* The hardcoded client version of the Android VR app used for InnerTube requests with this client.
*
* It can be extracted by getting the latest release version of the app on
* [the App Store page of the YouTube app](https://www.meta.com/en-us/experiences/2002317119880945/),
* in the `Additional details` section.
*/
private const val CLIENT_VERSION_ANDROID_VR = "1.61.48"
/**
* The device machine id for the Meta Quest 3, used to get opus codec with the Android VR client.
* See [this GitLab](https://dumps.tadiphone.dev/dumps/oculus/eureka) for more information.
*/
private const val DEVICE_MODEL_ANDROID_VR = "Quest 3"
private const val DEVICE_MAKE_ANDROID_VR = "Oculus"
private const val OS_VERSION_ANDROID_VR = "12"
/**
* The SDK version for Android 12 is 31,
* but for some reason the build.props for the `Quest 3` state that the SDK version is 32.
*/
private const val ANDROID_SDK_VERSION_ANDROID_VR = "32"
private val USER_AGENT_ANDROID_VR =
androidUserAgent(PACKAGE_NAME_ANDROID_VR, CLIENT_VERSION_ANDROID_VR, OS_VERSION_ANDROID_VR)
// ANDROID UNPLUGGED
/**
* Video not playable: Playlists / Music
* Note: Audio track is not available
*/
private const val PACKAGE_NAME_ANDROID_UNPLUGGED = "com.google.android.apps.youtube.unplugged"
private const val CLIENT_VERSION_ANDROID_UNPLUGGED = "8.16.0"
/**
* The device machine id for the Chromecast with Google TV 4K.
* See [this GitLab](https://dumps.tadiphone.dev/dumps/google/kirkwood) for more information.
*/
private const val DEVICE_MODEL_ANDROID_UNPLUGGED = "Google TV Streamer"
private const val DEVICE_MAKE_ANDROID_UNPLUGGED = "Google"
private const val OS_VERSION_ANDROID_UNPLUGGED = "14"
private const val ANDROID_SDK_VERSION_ANDROID_UNPLUGGED = "34"
private val USER_AGENT_ANDROID_UNPLUGGED = androidUserAgent(
PACKAGE_NAME_ANDROID_UNPLUGGED,
CLIENT_VERSION_ANDROID_UNPLUGGED,
OS_VERSION_ANDROID_UNPLUGGED
)
// ANDROID CREATOR
/**
* Video not playable: Livestream / HDR
* Note: Audio track is not available
*/
private const val PACKAGE_NAME_ANDROID_CREATOR = "com.google.android.apps.youtube.creator"
private const val CLIENT_VERSION_ANDROID_CREATOR = "23.47.101"
private val USER_AGENT_ANDROID_CREATOR = androidUserAgent(
PACKAGE_NAME_ANDROID_CREATOR,
CLIENT_VERSION_ANDROID_CREATOR
)
// ANDROID MUSIC
/**
* Video not playable: All videos that can't be played on YouTube Music
*/
private const val PACKAGE_NAME_ANDROID_MUSIC = "com.google.android.apps.youtube.music"
/**
* Older client versions don't seem to require poToken.
* It is not the default client yet, as it requires sufficient testing.
*/
private const val CLIENT_VERSION_ANDROID_MUSIC = "4.27.53"
private val USER_AGENT_ANDROID_MUSIC = androidUserAgent(
PACKAGE_NAME_ANDROID_MUSIC,
CLIENT_VERSION_ANDROID_MUSIC
)
private fun androidUserAgent(
packageName: String,
clientVersion: String,
osVersion: String? = Build.VERSION.RELEASE
): String {
return packageName +
"/" +
clientVersion +
" (Linux; U; Android " +
osVersion +
"; GB) gzip"
}
private fun iOSUserAgent(packageName: String, clientVersion: String): String {
return packageName +
"/" +
clientVersion +
"(" +
DEVICE_MODEL_IOS +
"; U; CPU iOS " +
USER_AGENT_VERSION_IOS +
" like Mac OS X)"
}
private fun forceAVC(): Boolean {
return BaseSettings.SPOOF_STREAMING_DATA_IOS_FORCE_AVC.get()
}
fun availableClientTypes(preferredClient: ClientType): Array<ClientType> {
val availableClientTypes = if (PatchStatus.SpoofStreamingDataMusic())
ClientType.CLIENT_ORDER_TO_USE_YOUTUBE_MUSIC
else
ClientType.CLIENT_ORDER_TO_USE_YOUTUBE
if (ArrayUtils.contains(availableClientTypes, preferredClient)) {
val clientToUse: Array<ClientType?> = arrayOfNulls(availableClientTypes.size)
clientToUse[0] = preferredClient
var i = 1
for (c in availableClientTypes) {
if (c != preferredClient) {
clientToUse[i++] = c
}
}
return clientToUse.filterNotNull().toTypedArray()
} else {
return availableClientTypes
}
}
@Suppress("DEPRECATION")
enum class ClientType(
/**
* [YouTube client type](https://github.com/zerodytrash/YouTube-Internal-Clients?tab=readme-ov-file#clients)
*/
val id: Int,
/**
* Device model, equivalent to [Build.MANUFACTURER] (System property: ro.product.vendor.manufacturer)
*/
val deviceMake: String = Build.MANUFACTURER,
/**
* Device model, equivalent to [Build.MODEL] (System property: ro.product.model)
*/
val deviceModel: String = Build.MODEL,
/**
* Device OS name.
*/
val osName: String = "Android",
/**
* Device OS version, equivalent to [Build.VERSION.RELEASE] (System property: ro.system.build.version.release)
*/
val osVersion: String = Build.VERSION.RELEASE,
/**
* Client user-agent.
*/
val userAgent: String,
/**
* Android SDK version, equivalent to [Build.VERSION.SDK] (System property: ro.build.version.sdk)
*/
val androidSdkVersion: String = Build.VERSION.SDK,
/**
* App version.
*/
val clientVersion: String,
/**
* If the client can access the API logged in.
* If false, 'Authorization' must not be included.
*/
val supportsCookies: Boolean = true,
/**
* If the client can only access the API logged in.
* If true, 'Authorization' must be included.
*/
val requireAuth: Boolean = false,
/**
* Whether a poToken is required to get playback for more than 1 minute.
*/
val requirePoToken: Boolean = false,
/**
* Friendly name displayed in stats for nerds.
*/
val friendlyName: String
) {
ANDROID_VR(
id = 28,
deviceMake = DEVICE_MAKE_ANDROID_VR,
deviceModel = DEVICE_MODEL_ANDROID_VR,
osVersion = OS_VERSION_ANDROID_VR,
userAgent = USER_AGENT_ANDROID_VR,
androidSdkVersion = ANDROID_SDK_VERSION_ANDROID_VR,
clientVersion = CLIENT_VERSION_ANDROID_VR,
friendlyName = "Android VR"
),
ANDROID_UNPLUGGED(
id = 29,
deviceMake = DEVICE_MAKE_ANDROID_UNPLUGGED,
deviceModel = DEVICE_MODEL_ANDROID_UNPLUGGED,
osVersion = OS_VERSION_ANDROID_UNPLUGGED,
userAgent = USER_AGENT_ANDROID_UNPLUGGED,
androidSdkVersion = ANDROID_SDK_VERSION_ANDROID_UNPLUGGED,
clientVersion = CLIENT_VERSION_ANDROID_UNPLUGGED,
requireAuth = true,
friendlyName = "Android TV"
),
ANDROID_CREATOR(
id = 14,
userAgent = USER_AGENT_ANDROID_CREATOR,
clientVersion = CLIENT_VERSION_ANDROID_CREATOR,
requireAuth = true,
friendlyName = "Android Studio"
),
IOS_UNPLUGGED(
id = 33,
deviceMake = DEVICE_MAKE_IOS,
deviceModel = DEVICE_MODEL_IOS,
osName = OS_NAME_IOS,
osVersion = OS_VERSION_IOS,
userAgent = USER_AGENT_IOS_UNPLUGGED,
clientVersion = CLIENT_VERSION_IOS_UNPLUGGED,
requireAuth = true,
friendlyName = if (forceAVC())
"iOS TV Force AVC"
else
"iOS TV"
),
IOS(
id = 5,
deviceMake = DEVICE_MAKE_IOS,
deviceModel = DEVICE_MODEL_IOS,
osName = OS_NAME_IOS,
osVersion = OS_VERSION_IOS,
userAgent = USER_AGENT_IOS,
clientVersion = CLIENT_VERSION_IOS,
supportsCookies = false,
requirePoToken = true,
friendlyName = if (forceAVC())
"iOS Force AVC"
else
"iOS"
),
ANDROID_MUSIC(
id = 21,
userAgent = USER_AGENT_ANDROID_MUSIC,
clientVersion = CLIENT_VERSION_ANDROID_MUSIC,
requireAuth = true,
friendlyName = "Android Music"
);
val clientName: String = name
companion object {
val CLIENT_ORDER_TO_USE_YOUTUBE: Array<ClientType> = arrayOf(
ANDROID_VR,
ANDROID_UNPLUGGED,
IOS_UNPLUGGED,
ANDROID_CREATOR,
IOS,
)
internal val CLIENT_ORDER_TO_USE_YOUTUBE_MUSIC: Array<ClientType> = arrayOf(
ANDROID_VR,
ANDROID_MUSIC,
)
}
}
}

View File

@ -0,0 +1,43 @@
package app.revanced.extension.shared.patches.client
/**
* Used to fetch video information.
*/
@Suppress("unused")
object WebClient {
/**
* This user agent does not require a PoToken in [ClientType.MWEB]
* https://github.com/yt-dlp/yt-dlp/blob/0b6b7742c2e7f2a1fcb0b54ef3dd484bab404b3f/yt_dlp/extractor/youtube.py#L259
*/
private const val USER_AGENT_SAFARI =
"Mozilla/5.0 (iPad; CPU OS 16_7_10 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1,gzip(gfe)"
enum class ClientType(
/**
* [YouTube client type](https://github.com/zerodytrash/YouTube-Internal-Clients?tab=readme-ov-file#clients)
*/
val id: Int,
/**
* Client user-agent.
*/
@JvmField
val userAgent: String = USER_AGENT_SAFARI,
/**
* Client version.
*/
@JvmField
val clientVersion: String
) {
MWEB(
id = 2,
clientVersion = "2.20241202.07.00"
),
WEB_REMIX(
id = 29,
clientVersion = "1.20241218.01.00",
);
@JvmField
val clientName: String = name
}
}

View File

@ -1,10 +1,12 @@
package app.revanced.extension.shared.patches.spoof; package app.revanced.extension.shared.patches.spoof;
import static app.revanced.extension.shared.patches.PatchStatus.SpoofStreamingData; import static app.revanced.extension.shared.patches.PatchStatus.SpoofStreamingDataMusic;
import android.net.Uri; import android.net.Uri;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Base64;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
@ -19,7 +21,13 @@ import app.revanced.extension.shared.utils.Utils;
@SuppressWarnings("unused") @SuppressWarnings("unused")
public class SpoofStreamingDataPatch { public class SpoofStreamingDataPatch {
public static final boolean SPOOF_STREAMING_DATA = SpoofStreamingData() && BaseSettings.SPOOF_STREAMING_DATA.get(); private static final boolean SPOOF_STREAMING_DATA = BaseSettings.SPOOF_STREAMING_DATA.get();
private static final boolean SPOOF_STREAMING_DATA_YOUTUBE = SPOOF_STREAMING_DATA && !SpoofStreamingDataMusic();
private static final boolean SPOOF_STREAMING_DATA_MUSIC = SPOOF_STREAMING_DATA && SpoofStreamingDataMusic();
private static final String PO_TOKEN =
BaseSettings.SPOOF_STREAMING_DATA_PO_TOKEN.get();
private static final String VISITOR_DATA =
BaseSettings.SPOOF_STREAMING_DATA_VISITOR_DATA.get();
/** /**
* Any unreachable ip address. Used to intentionally fail requests. * Any unreachable ip address. Used to intentionally fail requests.
@ -27,6 +35,9 @@ public class SpoofStreamingDataPatch {
private static final String UNREACHABLE_HOST_URI_STRING = "https://127.0.0.0"; private static final String UNREACHABLE_HOST_URI_STRING = "https://127.0.0.0";
private static final Uri UNREACHABLE_HOST_URI = Uri.parse(UNREACHABLE_HOST_URI_STRING); private static final Uri UNREACHABLE_HOST_URI = Uri.parse(UNREACHABLE_HOST_URI_STRING);
@NonNull
private static volatile String droidGuardPoToken = "";
/** /**
* Key: video id * Key: video id
* Value: original video length [streamingData.formats.approxDurationMs] * Value: original video length [streamingData.formats.approxDurationMs]
@ -50,6 +61,8 @@ public class SpoofStreamingDataPatch {
*/ */
public static Uri blockGetWatchRequest(Uri playerRequestUri) { public static Uri blockGetWatchRequest(Uri playerRequestUri) {
if (SPOOF_STREAMING_DATA) { if (SPOOF_STREAMING_DATA) {
// An exception may be thrown when the /get_watch request is blocked when connected to Wi-Fi in YouTube Music.
if (SPOOF_STREAMING_DATA_YOUTUBE || Utils.getNetworkType() == Utils.NetworkType.MOBILE) {
try { try {
String path = playerRequestUri.getPath(); String path = playerRequestUri.getPath();
@ -62,6 +75,7 @@ public class SpoofStreamingDataPatch {
Logger.printException(() -> "blockGetWatchRequest failure", ex); Logger.printException(() -> "blockGetWatchRequest failure", ex);
} }
} }
}
return playerRequestUri; return playerRequestUri;
} }
@ -108,30 +122,99 @@ public class SpoofStreamingDataPatch {
return false; return false;
} }
private static volatile String auth = "";
private static volatile Map<String, String> 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.
* <p>
* 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.
* <p>
*
* @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<String, String> 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. * Injection point.
*/ */
public static void fetchStreams(String url, Map<String, String> requestHeaders) { public static void fetchStreams(String url, Map<String, String> requestHeaders) {
setRequestHeaders(requestHeaders);
if (SPOOF_STREAMING_DATA) { if (SPOOF_STREAMING_DATA) {
try { try {
Uri uri = Uri.parse(url); Uri uri = Uri.parse(url);
String path = uri.getPath(); String path = uri.getPath();
if (path == null || !path.contains("player")) {
// '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; return;
} }
StreamingDataRequest.fetchRequest(id, requestHeaders); // '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.
// '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) { } 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)}. * Called after {@link #getStreamingData(String)}.
*/ */
public static void setApproxDurationMs(String videoId, long approxDurationMs) { 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); approxDurationMsMap.put(videoId, approxDurationMs);
Logger.printDebug(() -> "New approxDurationMs loaded, video id: " + videoId + ", video length: " + approxDurationMs); Logger.printDebug(() -> "New approxDurationMs loaded, video id: " + videoId + ", video length: " + approxDurationMs);
} }
@ -203,11 +286,10 @@ public class SpoofStreamingDataPatch {
* Called after {@link #getStreamingData(String)}. * Called after {@link #getStreamingData(String)}.
*/ */
public static long getApproxDurationMs(String videoId) { 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); final Long approxDurationMs = approxDurationMsMap.get(videoId);
if (approxDurationMs != null) { if (approxDurationMs != null) {
Logger.printDebug(() -> "Replacing video length: " + approxDurationMs + " for videoId: " + videoId); Logger.printDebug(() -> "Replacing video length: " + approxDurationMs + " for videoId: " + videoId);
approxDurationMsMap.remove(videoId);
return approxDurationMs; return approxDurationMs;
} }
} }
@ -253,4 +335,17 @@ public class SpoofStreamingDataPatch {
return videoFormat; 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;
}
}
}
} }

View File

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

View File

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

View File

@ -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.
* <p>
* 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<String, StreamingDataRequest> 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<ByteBuffer> future;
private StreamingDataRequest(String videoId, Map<String, String> playerHeaders) {
Objects.requireNonNull(playerHeaders);
this.videoId = videoId;
this.future = Utils.submitOnBackgroundThread(() -> fetch(videoId, playerHeaders));
}
public static void fetchRequest(String videoId, Map<String, String> 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<String, String> 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<String, String> 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 + '\'' + '}';
}
}

View File

@ -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<String, String>, visitorId: String,
botGuardPoToken: String, droidGuardPoToken: String
) {
private val videoId: String
private val future: Future<ByteBuffer?>
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<AppClient.ClientType> =
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<String, StreamingDataRequest> = Collections.synchronizedMap(
object : LinkedHashMap<String, StreamingDataRequest>(100) {
/**
* Cache limit must be greater than the maximum number of videos open at once,
* which theoretically is more than 4 (3 Shorts + one regular minimized video).
* But instead use a much larger value, to handle if a video viewed a while ago
* is somehow still referenced. Each stream is a small array of Strings
* so memory usage is not a concern.
*/
private val CACHE_LIMIT = 50
override fun removeEldestEntry(eldest: Map.Entry<String, StreamingDataRequest>): Boolean {
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<String, String>, 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<String, String>,
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<String, String>, 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
}
}
}

View File

@ -111,22 +111,21 @@ public final class RYDVoteData {
public void updateUsingVote(Vote vote) { public void updateUsingVote(Vote vote) {
final int likesToAdd, dislikesToAdd; final int likesToAdd, dislikesToAdd;
switch (vote) { dislikesToAdd = switch (vote) {
case LIKE: case LIKE -> {
likesToAdd = 1; likesToAdd = 1;
dislikesToAdd = 0; yield 0;
break;
case DISLIKE:
likesToAdd = 0;
dislikesToAdd = 1;
break;
case LIKE_REMOVE:
likesToAdd = 0;
dislikesToAdd = 0;
break;
default:
throw new IllegalStateException();
} }
case DISLIKE -> {
likesToAdd = 0;
yield 1;
}
case LIKE_REMOVE -> {
likesToAdd = 0;
yield 0;
}
default -> throw new IllegalStateException();
};
// If a video has no public likes but RYD has raw like data, // If a video has no public likes but RYD has raw like data,
// then use the raw data instead. // then use the raw data instead.

View File

@ -27,6 +27,7 @@ import app.revanced.extension.shared.returnyoutubedislike.ReturnYouTubeDislike;
import app.revanced.extension.shared.utils.Logger; import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.Utils; import app.revanced.extension.shared.utils.Utils;
@SuppressWarnings("All")
public class ReturnYouTubeDislikeApi { public class ReturnYouTubeDislikeApi {
/** /**
* {@link #fetchVotes(String)} TCP connection timeout * {@link #fetchVotes(String)} TCP connection timeout
@ -81,9 +82,87 @@ public class ReturnYouTubeDislikeApi {
public static boolean toastOnConnectionError = false; 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() { private ReturnYouTubeDislikeApi() {
} // utility class } // 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. * Clears any backoff rate limits in effect.
* Should be called if RYD is turned on/off. * 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 * @return True, if a client rate limit was requested
*/ */
private static boolean checkIfRateLimitWasHit(int httpResponseCode) { 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; 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) { if (connectionError && rateLimitHit) {
throw new IllegalArgumentException(); 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) { if (connectionError) {
timeToResumeAPICalls = System.currentTimeMillis() + BACKOFF_CONNECTION_ERROR_MILLISECONDS; timeToResumeAPICalls = System.currentTimeMillis() + BACKOFF_CONNECTION_ERROR_MILLISECONDS;
fetchCallResponseTimeLast = responseTimeOfFetchCall;
fetchCallNumberOfFailures++;
lastApiCallFailed = true; lastApiCallFailed = true;
} else if (rateLimitHit) { } else if (rateLimitHit) {
Logger.printDebug(() -> "API rate limit was hit. Stopping API calls for the next " Logger.printDebug(() -> "API rate limit was hit. Stopping API calls for the next "
+ BACKOFF_RATE_LIMIT_MILLISECONDS + " seconds"); + BACKOFF_RATE_LIMIT_MILLISECONDS + " seconds");
timeToResumeAPICalls = System.currentTimeMillis() + BACKOFF_RATE_LIMIT_MILLISECONDS; timeToResumeAPICalls = System.currentTimeMillis() + BACKOFF_RATE_LIMIT_MILLISECONDS;
numberOfRateLimitRequestsEncountered++;
fetchCallResponseTimeLast = FETCH_CALL_RESPONSE_TIME_VALUE_RATE_LIMIT;
if (!lastApiCallFailed && toastOnConnectionError) { if (!lastApiCallFailed && toastOnConnectionError) {
Utils.showToastLong(str("revanced_ryd_failure_client_rate_limit_requested")); Utils.showToastLong(str("revanced_ryd_failure_client_rate_limit_requested"));
} }
lastApiCallFailed = true; lastApiCallFailed = true;
} else { } else {
fetchCallResponseTimeLast = responseTimeOfFetchCall;
lastApiCallFailed = false; 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) { if (!lastApiCallFailed && toastOnConnectionError) {
if (showLongToast) {
Utils.showToastLong(toastMessage);
} else {
Utils.showToastShort(toastMessage); Utils.showToastShort(toastMessage);
} }
if (ex != null) { }
lastApiCallFailed = true;
Logger.printInfo(() -> toastMessage, ex); Logger.printInfo(() -> toastMessage, ex);
} }
}
/**
* @return NULL if fetch failed, or if a rate limit is in effect.
*/
@Nullable @Nullable
public static RYDVoteData fetchVotes(String videoId) { public static RYDVoteData fetchVotes(String videoId) {
Utils.verifyOffMainThread(); Utils.verifyOffMainThread();
@ -160,6 +262,7 @@ public class ReturnYouTubeDislikeApi {
return null; return null;
} }
Logger.printDebug(() -> "Fetching votes for: " + videoId); Logger.printDebug(() -> "Fetching votes for: " + videoId);
final long timeNetworkCallStarted = System.currentTimeMillis();
try { try {
HttpURLConnection connection = getRYDConnectionFromRoute(ReturnYouTubeDislikeRoutes.GET_DISLIKES, videoId); 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.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 connection.setReadTimeout(API_GET_VOTES_HTTP_TIMEOUT_MILLISECONDS); // timeout for server response
randomlyWaitIfLocallyDebugging();
final int responseCode = connection.getResponseCode(); final int responseCode = connection.getResponseCode();
if (checkIfRateLimitWasHit(responseCode)) { if (checkIfRateLimitWasHit(responseCode)) {
connection.disconnect(); // rate limit hit, should disconnect connection.disconnect(); // rate limit hit, should disconnect
updateRateLimitAndStats(false, true); updateRateLimitAndStats(timeNetworkCallStarted, false, true);
return null; return null;
} }
@ -185,7 +290,7 @@ public class ReturnYouTubeDislikeApi {
JSONObject json = Requester.parseJSONObject(connection); JSONObject json = Requester.parseJSONObject(connection);
try { try {
RYDVoteData votingData = new RYDVoteData(json); RYDVoteData votingData = new RYDVoteData(json);
updateRateLimitAndStats(false, false); updateRateLimitAndStats(timeNetworkCallStarted, false, false);
Logger.printDebug(() -> "Voting data fetched: " + votingData); Logger.printDebug(() -> "Voting data fetched: " + votingData);
return votingData; return votingData;
} catch (JSONException ex) { } catch (JSONException ex) {
@ -193,20 +298,21 @@ public class ReturnYouTubeDislikeApi {
// fall thru to update statistics // fall thru to update statistics
} }
} else { } 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 connection.disconnect(); // Something went wrong, might as well disconnect.
} catch ( } catch (SocketTimeoutException ex) {
SocketTimeoutException ex) { // connection timed out, response timeout, or some other network error handleConnectionError((str("revanced_ryd_failure_connection_timeout")), ex, false);
handleConnectionError((str("revanced_ryd_failure_connection_timeout")), ex);
} catch (IOException ex) { } 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) { } catch (Exception ex) {
// should never happen // should never happen
Logger.printException(() -> "fetchVotes failure", ex); Logger.printException(() -> "fetchVotes failure", ex);
} }
updateRateLimitAndStats(true, false); updateRateLimitAndStats(timeNetworkCallStarted, true, false);
return null; return null;
} }
@ -220,7 +326,7 @@ public class ReturnYouTubeDislikeApi {
if (checkIfRateLimitInEffect("registerAsNewUser")) { if (checkIfRateLimitInEffect("registerAsNewUser")) {
return null; return null;
} }
String userId = randomString(); String userId = randomString(36);
Logger.printDebug(() -> "Trying to register new user"); Logger.printDebug(() -> "Trying to register new user");
HttpURLConnection connection = getRYDConnectionFromRoute(ReturnYouTubeDislikeRoutes.GET_REGISTRATION, userId); HttpURLConnection connection = getRYDConnectionFromRoute(ReturnYouTubeDislikeRoutes.GET_REGISTRATION, userId);
@ -241,12 +347,13 @@ public class ReturnYouTubeDislikeApi {
String solution = solvePuzzle(challenge, difficulty); String solution = solvePuzzle(challenge, difficulty);
return confirmRegistration(userId, solution); 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(); connection.disconnect();
} catch (SocketTimeoutException ex) { } catch (SocketTimeoutException ex) {
handleConnectionError(str("revanced_ryd_failure_connection_timeout"), ex); handleConnectionError(str("revanced_ryd_failure_connection_timeout"), ex, false);
} catch (IOException ex) { } 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) { } catch (Exception ex) {
Logger.printException(() -> "Failed to register user", ex); // should never happen Logger.printException(() -> "Failed to register user", ex); // should never happen
} }
@ -283,15 +390,18 @@ public class ReturnYouTubeDislikeApi {
Logger.printDebug(() -> "Registration confirmation successful"); Logger.printDebug(() -> "Registration confirmation successful");
return userId; return userId;
} }
// Something went wrong, might as well disconnect. // Something went wrong, might as well disconnect.
String response = Requester.parseStringAndDisconnect(connection); String response = Requester.parseStringAndDisconnect(connection);
Logger.printInfo(() -> "Failed to confirm registration for user: " + userId Logger.printInfo(() -> "Failed to confirm registration for user: " + userId
+ " solution: " + solution + " responseCode: " + responseCode + " response: '" + response + "''"); + " 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) { } catch (SocketTimeoutException ex) {
handleConnectionError(str("revanced_ryd_failure_connection_timeout"), ex); handleConnectionError(str("revanced_ryd_failure_connection_timeout"), ex, false);
} catch (IOException ex) { } 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) { } catch (Exception ex) {
Logger.printException(() -> "Failed to confirm registration for user: " + userId Logger.printException(() -> "Failed to confirm registration for user: " + userId
+ "solution: " + solution, ex); + "solution: " + solution, ex);
@ -299,16 +409,16 @@ public class ReturnYouTubeDislikeApi {
return null; 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(); Utils.verifyOffMainThread();
Objects.requireNonNull(videoId); Objects.requireNonNull(videoId);
Objects.requireNonNull(vote); Objects.requireNonNull(vote);
try { try {
if (userId == null) return; if (userId == null) return false;
if (checkIfRateLimitInEffect("sendVote")) { if (checkIfRateLimitInEffect("sendVote")) {
return; return false;
} }
Logger.printDebug(() -> "Trying to vote for video: " + videoId + " with vote: " + vote); Logger.printDebug(() -> "Trying to vote for video: " + videoId + " with vote: " + vote);
@ -325,7 +435,7 @@ public class ReturnYouTubeDislikeApi {
final int responseCode = connection.getResponseCode(); final int responseCode = connection.getResponseCode();
if (checkIfRateLimitWasHit(responseCode)) { if (checkIfRateLimitWasHit(responseCode)) {
connection.disconnect(); // disconnect, as no more connections will be made for a little while connection.disconnect(); // disconnect, as no more connections will be made for a little while
return; return false;
} }
if (responseCode == HTTP_STATUS_CODE_SUCCESS) { if (responseCode == HTTP_STATUS_CODE_SUCCESS) {
JSONObject json = Requester.parseJSONObject(connection); JSONObject json = Requester.parseJSONObject(connection);
@ -333,25 +443,26 @@ public class ReturnYouTubeDislikeApi {
int difficulty = json.getInt("difficulty"); int difficulty = json.getInt("difficulty");
String solution = solvePuzzle(challenge, difficulty); String solution = solvePuzzle(challenge, difficulty);
confirmVote(videoId, userId, solution); return confirmVote(videoId, userId, solution);
return;
} }
Logger.printInfo(() -> "Failed to send vote for video: " + videoId + " vote: " + vote Logger.printInfo(() -> "Failed to send vote for video: " + videoId + " vote: " + vote
+ " response code was: " + responseCode); + " 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 connection.disconnect(); // something went wrong, might as well disconnect
} catch (SocketTimeoutException ex) { } catch (SocketTimeoutException ex) {
handleConnectionError(str("revanced_ryd_failure_connection_timeout"), ex); handleConnectionError(str("revanced_ryd_failure_connection_timeout"), ex, false);
} catch (IOException ex) { } 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) { } catch (Exception ex) {
// should never happen // should never happen
Logger.printException(() -> "Failed to send vote for video: " + videoId + " vote: " + vote, ex); 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(); Utils.verifyOffMainThread();
Objects.requireNonNull(videoId); Objects.requireNonNull(videoId);
Objects.requireNonNull(userId); Objects.requireNonNull(userId);
@ -359,7 +470,7 @@ public class ReturnYouTubeDislikeApi {
try { try {
if (checkIfRateLimitInEffect("confirmVote")) { if (checkIfRateLimitInEffect("confirmVote")) {
return; return false;
} }
Logger.printDebug(() -> "Trying to confirm vote for video: " + videoId + " solution: " + solution); Logger.printDebug(() -> "Trying to confirm vote for video: " + videoId + " solution: " + solution);
HttpURLConnection connection = getRYDConnectionFromRoute(ReturnYouTubeDislikeRoutes.CONFIRM_VOTE); HttpURLConnection connection = getRYDConnectionFromRoute(ReturnYouTubeDislikeRoutes.CONFIRM_VOTE);
@ -375,25 +486,29 @@ public class ReturnYouTubeDislikeApi {
final int responseCode = connection.getResponseCode(); final int responseCode = connection.getResponseCode();
if (checkIfRateLimitWasHit(responseCode)) { if (checkIfRateLimitWasHit(responseCode)) {
connection.disconnect(); // disconnect, as no more connections will be made for a little while connection.disconnect(); // disconnect, as no more connections will be made for a little while
return; return false;
} }
if (responseCode == HTTP_STATUS_CODE_SUCCESS) { if (responseCode == HTTP_STATUS_CODE_SUCCESS) {
Logger.printDebug(() -> "Vote confirm successful for video: " + videoId); Logger.printDebug(() -> "Vote confirm successful for video: " + videoId);
return; return true;
} }
// Something went wrong, might as well disconnect. // Something went wrong, might as well disconnect.
String response = Requester.parseStringAndDisconnect(connection); String response = Requester.parseStringAndDisconnect(connection);
Logger.printInfo(() -> "Failed to confirm vote for video: " + videoId Logger.printInfo(() -> "Failed to confirm vote for video: " + videoId
+ " solution: " + solution + " responseCode: " + responseCode + " response: '" + response + "'"); + " 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) { } catch (SocketTimeoutException ex) {
handleConnectionError(str("revanced_ryd_failure_connection_timeout"), ex); handleConnectionError(str("revanced_ryd_failure_connection_timeout"), ex, false);
} catch (IOException ex) { } 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) { } catch (Exception ex) {
Logger.printException(() -> "Failed to confirm vote for video: " + videoId Logger.printException(() -> "Failed to confirm vote for video: " + videoId
+ " solution: " + solution, ex); // should never happen + " solution: " + solution, ex); // should never happen
} }
return false;
} }
private static void applyCommonPostRequestSettings(HttpURLConnection connection) throws ProtocolException { private static void applyCommonPostRequestSettings(HttpURLConnection connection) throws ProtocolException {
@ -440,12 +555,12 @@ public class ReturnYouTubeDislikeApi {
} }
// https://stackoverflow.com/a/157202 // https://stackoverflow.com/a/157202
private static String randomString() { private static String randomString(int len) {
String AB = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; String AB = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
SecureRandom rnd = new SecureRandom(); SecureRandom rnd = new SecureRandom();
StringBuilder sb = new StringBuilder(36); StringBuilder sb = new StringBuilder(len);
for (int i = 0; i < 36; i++) for (int i = 0; i < len; i++)
sb.append(AB.charAt(rnd.nextInt(AB.length()))); sb.append(AB.charAt(rnd.nextInt(AB.length())));
return sb.toString(); return sb.toString();
} }

View File

@ -3,7 +3,6 @@ package app.revanced.extension.shared.settings;
import static java.lang.Boolean.FALSE; import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE; import static java.lang.Boolean.TRUE;
import static app.revanced.extension.shared.patches.PatchStatus.HideFullscreenAdsDefaultBoolean; 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.ReturnYouTubeUsernamePatch.DisplayFormat;
import app.revanced.extension.shared.patches.client.AppClient.ClientType; 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 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 BooleanSetting SPOOF_STREAMING_DATA = new BooleanSetting("revanced_spoof_streaming_data", TRUE, true, "revanced_spoof_streaming_data_user_dialog_message");
public static final EnumSetting<ClientType> SPOOF_STREAMING_DATA_TYPE = new EnumSetting<>("revanced_spoof_streaming_data_type", ClientType.ANDROID_VR, true); public static final BooleanSetting SPOOF_STREAMING_DATA_IOS_FORCE_AVC = new BooleanSetting("revanced_spoof_streaming_data_ios_force_avc", FALSE, 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"); "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); 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<ClientType> 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 * @noinspection DeprecatedIsStillUsed

View File

@ -1,5 +1,6 @@
package app.revanced.extension.shared.utils; package app.revanced.extension.shared.utils;
import android.app.Activity;
import android.content.Context; import android.content.Context;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.view.animation.Animation; import android.view.animation.Animation;
@ -16,12 +17,25 @@ public class ResourceUtils extends Utils {
} // utility class } // utility class
public static int getIdentifier(@NonNull String str, @NonNull ResourceType resourceType) { 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, public static int getIdentifier(@NonNull String str, @NonNull ResourceType resourceType,
@NonNull Context context) { @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) { public static int getAnimIdentifier(@NonNull String str) {

View File

@ -1,6 +1,7 @@
package app.revanced.extension.shared.utils; package app.revanced.extension.shared.utils;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context; import android.content.Context;
import android.content.res.Resources; import android.content.res.Resources;
@ -104,7 +105,10 @@ public class StringRef extends Utils {
public String toString() { public String toString() {
if (!resolved) { if (!resolved) {
try { try {
Context context = getContext(); Activity mActivity = getActivity();
Context context = mActivity != null
? mActivity
: getContext();
if (resources == null) { if (resources == null) {
resources = getResources(); resources = getResources();
} }

View File

@ -54,8 +54,6 @@ public class Utils {
@SuppressLint("StaticFieldLeak") @SuppressLint("StaticFieldLeak")
public static Context context; public static Context context;
private static Resources resources;
protected Utils() { protected Utils() {
} // utility class } // utility class
@ -142,6 +140,24 @@ public class Utils {
return backgroundThreadPool.submit(call); 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) { public static boolean containsAny(@NonNull String value, @NonNull String... targets) {
return indexOfFirstFound(value, targets) >= 0; return indexOfFirstFound(value, targets) >= 0;
@ -264,11 +280,15 @@ public class Utils {
} }
public static Resources getResources() { public static Resources getResources() {
if (resources == null) { Activity mActivity = activityRef.get();
return getLocalizedContextAndSetResources(getContext()).getResources(); if (mActivity != null) {
} else { return mActivity.getResources();
return resources;
} }
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) { if (mActivity == null) {
return mContext; return mContext;
} }
if (mContext == null) {
return null;
}
// Locale of MainActivity. // Locale of MainActivity.
Locale applicationLocale; Locale applicationLocale;
@ -303,7 +326,6 @@ public class Utils {
// If they are identical, no need to override them. // If they are identical, no need to override them.
if (applicationLocale == contextLocale) { if (applicationLocale == contextLocale) {
resources = mActivity.getResources();
return mContext; return mContext;
} }
@ -311,9 +333,7 @@ public class Utils {
Locale.setDefault(applicationLocale); Locale.setDefault(applicationLocale);
Configuration configuration = new Configuration(mContext.getResources().getConfiguration()); Configuration configuration = new Configuration(mContext.getResources().getConfiguration());
configuration.setLocale(applicationLocale); configuration.setLocale(applicationLocale);
Context localizedContext = mContext.createConfigurationContext(configuration); return mContext.createConfigurationContext(configuration);
resources = localizedContext.getResources();
return localizedContext;
} }
public static void setActivity(Activity mainActivity) { public static void setActivity(Activity mainActivity) {
@ -462,16 +482,6 @@ public class Utils {
return false; 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. * @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. * Automatically logs any exceptions the runnable throws.
* *

View File

@ -18,6 +18,7 @@ public final class CarouselShelfFilter extends Filter {
private static final String BROWSE_ID_CLIP = "FEclips"; private static final String BROWSE_ID_CLIP = "FEclips";
private static final String BROWSE_ID_HOME = "FEwhat_to_watch"; private static final String BROWSE_ID_HOME = "FEwhat_to_watch";
private static final String BROWSE_ID_LIBRARY = "FElibrary"; 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 = "FEactivity";
private static final String BROWSE_ID_NOTIFICATION_INBOX = "FEnotifications_inbox"; private static final String BROWSE_ID_NOTIFICATION_INBOX = "FEnotifications_inbox";
private static final String BROWSE_ID_PLAYLIST = "VLPL"; private static final String BROWSE_ID_PLAYLIST = "VLPL";
@ -32,9 +33,10 @@ public final class CarouselShelfFilter extends Filter {
); );
private static final Supplier<Stream<String>> whitelistBrowseId = () -> Stream.of( private static final Supplier<Stream<String>> whitelistBrowseId = () -> Stream.of(
BROWSE_ID_LIBRARY,
BROWSE_ID_NOTIFICATION_INBOX,
BROWSE_ID_CLIP, BROWSE_ID_CLIP,
BROWSE_ID_LIBRARY,
BROWSE_ID_MOVIE,
BROWSE_ID_NOTIFICATION_INBOX,
BROWSE_ID_PREMIUM BROWSE_ID_PREMIUM
); );

View File

@ -32,12 +32,15 @@ public final class ChangeStartPagePatch {
HISTORY("FEhistory", TRUE), HISTORY("FEhistory", TRUE),
LIBRARY("FElibrary", TRUE), LIBRARY("FElibrary", TRUE),
MOVIE("FEstorefront", TRUE), MOVIE("FEstorefront", TRUE),
NOTIFICATIONS("FEactivity", TRUE),
SUBSCRIPTIONS("FEsubscriptions", TRUE), SUBSCRIPTIONS("FEsubscriptions", TRUE),
TRENDING("FEtrending", TRUE), TRENDING("FEtrending", TRUE),
YOUR_CLIPS("FEclips", TRUE),
/** /**
* Channel id, this can be used as a browseId. * Channel id, this can be used as a browseId.
*/ */
COURSES("UCtFRv9O2AHqOZjjynzrv-xg", TRUE),
GAMING("UCOpNcN46UbXVtpKMrmU4Abg", TRUE), GAMING("UCOpNcN46UbXVtpKMrmU4Abg", TRUE),
LIVE("UC4R8DWoMoI7CAwX8_LjQHig", TRUE), LIVE("UC4R8DWoMoI7CAwX8_LjQHig", TRUE),
MUSIC("UC-9-kyTW8ZkZNDHQJ6FgpwQ", TRUE), MUSIC("UC-9-kyTW8ZkZNDHQJ6FgpwQ", TRUE),

View File

@ -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.hideViewByLayoutParams;
import static app.revanced.extension.shared.utils.Utils.hideViewGroupByMarginLayoutParams; 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.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.patches.utils.PatchStatus.ImageSearchButton;
import static app.revanced.extension.youtube.shared.NavigationBar.NavigationButton; import static app.revanced.extension.youtube.shared.NavigationBar.NavigationButton;
@ -41,10 +40,10 @@ import java.util.EnumMap;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.ResourceUtils; import app.revanced.extension.shared.utils.ResourceUtils;
import app.revanced.extension.shared.utils.Utils; import app.revanced.extension.shared.utils.Utils;
import app.revanced.extension.youtube.settings.Settings; import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.utils.ExtendedUtils;
import app.revanced.extension.youtube.utils.ThemeUtils; import app.revanced.extension.youtube.utils.ThemeUtils;
@SuppressWarnings("unused") @SuppressWarnings("unused")
@ -109,12 +108,7 @@ public class GeneralPatch {
// region [Disable splash animation] patch // region [Disable splash animation] patch
public static boolean disableSplashAnimation(boolean original) { public static boolean disableSplashAnimation(boolean original) {
try {
return !Settings.DISABLE_SPLASH_ANIMATION.get() && original; return !Settings.DISABLE_SPLASH_ANIMATION.get() && original;
} catch (Exception ex) {
Logger.printException(() -> "Failed to load disableSplashAnimation", ex);
}
return original;
} }
// endregion // endregion
@ -129,6 +123,10 @@ public class GeneralPatch {
// region [Hide layout components] patch // region [Hide layout components] patch
public static boolean disableTranslucentStatusBar(boolean original) {
return !Settings.DISABLE_TRANSLUCENT_STATUS_BAR.get() && original;
}
private static String[] accountMenuBlockList; private static String[] accountMenuBlockList;
static { static {
@ -198,8 +196,6 @@ public class GeneralPatch {
// region [Hide navigation bar components] patch // region [Hide navigation bar components] patch
private static final int fillBellCairoBlack = ResourceUtils.getDrawableIdentifier("yt_fill_bell_cairo_black_24");
private static final Map<NavigationButton, Boolean> shouldHideMap = new EnumMap<>(NavigationButton.class) { private static final Map<NavigationButton, Boolean> shouldHideMap = new EnumMap<>(NavigationButton.class) {
{ {
put(NavigationButton.HOME, Settings.HIDE_NAVIGATION_HOME_BUTTON.get()); put(NavigationButton.HOME, Settings.HIDE_NAVIGATION_HOME_BUTTON.get());
@ -215,10 +211,15 @@ public class GeneralPatch {
return Settings.ENABLE_NARROW_NAVIGATION_BUTTONS.get() || original; return Settings.ENABLE_NARROW_NAVIGATION_BUTTONS.get() || original;
} }
public static boolean enableTranslucentNavigationBar() {
return Settings.ENABLE_TRANSLUCENT_NAVIGATION_BAR.get();
}
/** /**
* @noinspection ALL * @noinspection ALL
*/ */
public static void setCairoNotificationFilledIcon(EnumMap enumMap, Enum tabActivityCairo) { public static void setCairoNotificationFilledIcon(EnumMap enumMap, Enum tabActivityCairo) {
final int fillBellCairoBlack = ResourceUtils.getDrawableIdentifier("yt_fill_bell_cairo_black_24");
if (fillBellCairoBlack != 0) { if (fillBellCairoBlack != 0) {
// It's very unlikely, but Google might fix this issue someday. // It's very unlikely, but Google might fix this issue someday.
// If so, [fillBellCairoBlack] might already be in enumMap. // If so, [fillBellCairoBlack] might already be in enumMap.
@ -245,48 +246,6 @@ public class GeneralPatch {
hideViewUnderCondition(Settings.HIDE_NAVIGATION_BAR.get(), view); 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 // endregion
// region [Remove viewer discretion dialog] patch // region [Remove viewer discretion dialog] patch
@ -428,6 +387,7 @@ public class GeneralPatch {
public static void setWideSearchBarLayout(View view) { public static void setWideSearchBarLayout(View view) {
if (!wideSearchbarEnabled) if (!wideSearchbarEnabled)
return; return;
if (!(view.findViewById(searchBarId) instanceof RelativeLayout searchBarView)) if (!(view.findViewById(searchBarId) instanceof RelativeLayout searchBarView))
return; return;
@ -550,12 +510,27 @@ public class GeneralPatch {
private static final int settingsDrawableId = private static final int settingsDrawableId =
ResourceUtils.getDrawableIdentifier("yt_outline_gear_black_24"); 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) { public static int getCreateButtonDrawableId(int original) {
return Settings.REPLACE_TOOLBAR_CREATE_BUTTON.get() && if (!Settings.REPLACE_TOOLBAR_CREATE_BUTTON.get()) {
settingsDrawableId != 0 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 ? settingsDrawableId
: original; : settingsCairoDrawableId;
} }
public static void replaceCreateButton(String enumString, View toolbarView) { public static void replaceCreateButton(String enumString, View toolbarView) {

View File

@ -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.IS_19_29_OR_GREATER;
import static app.revanced.extension.youtube.utils.ExtendedUtils.validateValue; import static app.revanced.extension.youtube.utils.ExtendedUtils.validateValue;
import android.content.Context;
import android.content.res.Resources;
import android.util.DisplayMetrics; import android.util.DisplayMetrics;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@ -82,11 +84,24 @@ public final class MiniplayerPatch {
} }
} }
private static final int MINIPLAYER_SIZE; private static int MINIPLAYER_SIZE = 0;
static { static {
setMiniPlayerSize();
}
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. // YT appears to use the device screen dip width, plus an unknown fixed horizontal padding size.
DisplayMetrics displayMetrics = Utils.getContext().getResources().getDisplayMetrics(); DisplayMetrics displayMetrics = resources.getDisplayMetrics();
final int deviceDipWidth = (int) (displayMetrics.widthPixels / displayMetrics.density); final int deviceDipWidth = (int) (displayMetrics.widthPixels / displayMetrics.density);
// YT seems to use a minimum height to calculate the minimum miniplayer width based on the video. // YT seems to use a minimum height to calculate the minimum miniplayer width based on the video.
@ -116,15 +131,9 @@ public final class MiniplayerPatch {
} }
MINIPLAYER_SIZE = dipWidth; MINIPLAYER_SIZE = dipWidth;
} catch (Exception ex) {
final int opacity = validateValue( Logger.printException(() -> "setMiniPlayerSize failure", ex);
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; 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 { public static final class MiniplayerHorizontalDragAvailability implements Setting.Availability {
@Override @Override
public boolean isAvailable() { public boolean isAvailable() {
@ -293,8 +313,13 @@ public final class MiniplayerPatch {
*/ */
public static int setMiniplayerDefaultSize(int original) { public static int setMiniplayerDefaultSize(int original) {
if (CURRENT_TYPE.isModern()) { if (CURRENT_TYPE.isModern()) {
if (MINIPLAYER_SIZE == 0) {
setMiniPlayerSize();
}
if (MINIPLAYER_SIZE != 0) {
return MINIPLAYER_SIZE; return MINIPLAYER_SIZE;
} }
}
return original; return original;
} }

View File

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

View File

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

View File

@ -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.shared.utils.Utils.hideViewUnderCondition;
import static app.revanced.extension.youtube.utils.ExtendedUtils.validateValue; 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.support.v7.widget.RecyclerView;
import android.util.TypedValue; import android.util.TypedValue;
import android.view.View; import android.view.View;
@ -20,7 +18,6 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.coordinatorlayout.widget.CoordinatorLayout; import androidx.coordinatorlayout.widget.CoordinatorLayout;
import java.lang.ref.WeakReference;
import java.util.Objects; import java.util.Objects;
import app.revanced.extension.shared.settings.BaseSettings; import app.revanced.extension.shared.settings.BaseSettings;
@ -328,37 +325,6 @@ public class PlayerPatch {
Utils.runOnMainThreadDelayed(() -> isScreenOn = false, Settings.KEEP_LANDSCAPE_MODE_TIMEOUT.get()); Utils.runOnMainThreadDelayed(() -> isScreenOn = false, Settings.KEEP_LANDSCAPE_MODE_TIMEOUT.get());
} }
private static WeakReference<Activity> 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 // endregion
// region [Hide comments component] patch // region [Hide comments component] patch

View File

@ -27,8 +27,22 @@ public class SwipeControlsPatch {
/** /**
* Injection point. * Injection point.
*/ */
public static boolean disableWatchPanelGestures() { public static boolean disableSwipeToEnterFullscreenModeBelowThePlayer() {
return !Settings.DISABLE_WATCH_PANEL_GESTURES.get(); 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;
} }
/** /**

View File

@ -7,11 +7,6 @@ public class PatchStatus {
return false; 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() { public static boolean PlayerButtons() {
// Replace this with true if the Hide player buttons patch succeeds // Replace this with true if the Hide player buttons patch succeeds
return false; return false;

View File

@ -106,7 +106,7 @@ public class PlayerControlsPatch {
} }
} }
private static void changeVisibilityNegatedImmediately() { public static void changeVisibilityNegatedImmediately() {
// AlwaysRepeat.changeVisibilityNegatedImmediate(); // AlwaysRepeat.changeVisibilityNegatedImmediate();
// CopyVideoUrl.changeVisibilityNegatedImmediate(); // CopyVideoUrl.changeVisibilityNegatedImmediate();
// CopyVideoUrlTimestamp.changeVisibilityNegatedImmediate(); // CopyVideoUrlTimestamp.changeVisibilityNegatedImmediate();

View File

@ -360,8 +360,8 @@ public class ReturnYouTubeDislikePatch {
removeRollingNumberPatchChanges(view); removeRollingNumberPatchChanges(view);
return original; return original;
} }
final boolean isDescriptionPanel = view.getParent() instanceof ViewGroup viewGroupParent final boolean isDescriptionPanel = view.getParent() instanceof ViewGroup viewGroupParent &&
&& viewGroupParent.getChildCount() < 2; viewGroupParent.getChildCount() < 2;
// Called for all instances of RollingNumber, so must check if text is for a dislikes. // 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. // Text will already have the correct content but it's missing the drawable separators.
if (!ReturnYouTubeDislike.isPreviouslyCreatedSegmentedSpan(original.toString()) || isDescriptionPanel) { if (!ReturnYouTubeDislike.isPreviouslyCreatedSegmentedSpan(original.toString()) || isDescriptionPanel) {

View File

@ -10,13 +10,15 @@ import org.apache.commons.lang3.BooleanUtils;
import app.revanced.extension.shared.utils.Logger; import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.Utils; import app.revanced.extension.shared.utils.Utils;
import app.revanced.extension.youtube.patches.utils.PatchStatus; 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.settings.Settings;
import app.revanced.extension.youtube.shared.VideoInformation; import app.revanced.extension.youtube.shared.VideoInformation;
import app.revanced.extension.youtube.whitelist.Whitelist; import app.revanced.extension.youtube.whitelist.Whitelist;
@SuppressWarnings("unused") @SuppressWarnings("unused")
public class PlaybackSpeedPatch { 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 final long TOAST_DELAY_MILLISECONDS = 750;
private static long lastTimeSpeedChanged; private static long lastTimeSpeedChanged;
private static boolean isLiveStream; private static boolean isLiveStream;
@ -39,8 +41,8 @@ public class PlaybackSpeedPatch {
/** /**
* Injection point. * Injection point.
*/ */
public static void fetchPlaylistData(@NonNull String videoId, boolean isShortAndOpeningOrPlaying) { public static void fetchMusicRequest(@NonNull String videoId, boolean isShortAndOpeningOrPlaying) {
if (Settings.DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC.get()) { if (DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC) {
try { try {
final boolean videoIdIsShort = VideoInformation.lastPlayerResponseIsShort(); final boolean videoIdIsShort = VideoInformation.lastPlayerResponseIsShort();
// Shorts shelf in home and subscription feed causes player response hook to be called, // Shorts shelf in home and subscription feed causes player response hook to be called,
@ -50,9 +52,12 @@ public class PlaybackSpeedPatch {
return; return;
} }
PlaylistRequest.fetchRequestIfNeeded(videoId); MusicRequest.fetchRequestIfNeeded(
videoId,
Settings.DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC_TYPE.get()
);
} catch (Exception ex) { } catch (Exception ex) {
Logger.printException(() -> "fetchPlaylistData failure", ex); Logger.printException(() -> "fetchMusicRequest failure", ex);
} }
} }
} }
@ -61,17 +66,18 @@ public class PlaybackSpeedPatch {
* Injection point. * Injection point.
*/ */
public static float getPlaybackSpeedInShorts(final float playbackSpeed) { public static float getPlaybackSpeedInShorts(final float playbackSpeed) {
if (!VideoInformation.lastPlayerResponseIsShort()) if (VideoInformation.lastPlayerResponseIsShort() &&
return playbackSpeed; Settings.ENABLE_DEFAULT_PLAYBACK_SPEED_SHORTS.get()
if (!Settings.ENABLE_DEFAULT_PLAYBACK_SPEED_SHORTS.get()) ) {
return playbackSpeed;
float defaultPlaybackSpeed = getDefaultPlaybackSpeed(VideoInformation.getChannelId(), null); float defaultPlaybackSpeed = getDefaultPlaybackSpeed(VideoInformation.getChannelId(), null);
Logger.printDebug(() -> "overridePlaybackSpeed in Shorts: " + defaultPlaybackSpeed); Logger.printDebug(() -> "overridePlaybackSpeed in Shorts: " + defaultPlaybackSpeed);
return defaultPlaybackSpeed; return defaultPlaybackSpeed;
} }
return playbackSpeed;
}
/** /**
* Injection point. * Injection point.
* Called when user selects a playback speed. * Called when user selects a playback speed.
@ -118,23 +124,21 @@ public class PlaybackSpeedPatch {
} }
private static float getDefaultPlaybackSpeed(@NonNull String channelId, @Nullable String videoId) { private static float getDefaultPlaybackSpeed(@NonNull String channelId, @Nullable String videoId) {
return (Settings.DISABLE_DEFAULT_PLAYBACK_SPEED_LIVE.get() && isLiveStream) || return (isLiveStream || Whitelist.isChannelWhitelistedPlaybackSpeed(channelId) || isMusic(videoId))
Whitelist.isChannelWhitelistedPlaybackSpeed(channelId) ||
getPlaylistData(videoId)
? 1.0f ? 1.0f
: Settings.DEFAULT_PLAYBACK_SPEED.get(); : Settings.DEFAULT_PLAYBACK_SPEED.get();
} }
private static boolean getPlaylistData(@Nullable String videoId) { private static boolean isMusic(@Nullable String videoId) {
if (Settings.DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC.get() && videoId != null) { if (DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC && videoId != null) {
try { try {
PlaylistRequest request = PlaylistRequest.getRequestForVideoId(videoId); MusicRequest request = MusicRequest.getRequestForVideoId(videoId);
final boolean isPlaylist = request != null && BooleanUtils.toBoolean(request.getStream()); final boolean isMusic = request != null && BooleanUtils.toBoolean(request.getStream());
Logger.printDebug(() -> "isPlaylist: " + isPlaylist); Logger.printDebug(() -> "videoId: " + videoId + ", isMusic: " + isMusic);
return isPlaylist; return isMusic;
} catch (Exception ex) { } catch (Exception ex) {
Logger.printException(() -> "getPlaylistData failure", ex); Logger.printException(() -> "getMusicRequest failure", ex);
} }
} }

View File

@ -38,7 +38,7 @@ public class VideoQualityPatch {
if (videoId.equals(newlyLoadedVideoId)) if (videoId.equals(newlyLoadedVideoId))
return; return;
videoId = newlyLoadedVideoId; videoId = newlyLoadedVideoId;
setVideoQuality(Settings.SKIP_PRELOADED_BUFFER.get() ? 250 : 500); setVideoQuality(Settings.SKIP_PRELOADED_BUFFER.get() ? 250 : 750);
} }
/** /**

View File

@ -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<Boolean> = 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<String, MusicRequest> = 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
}
}
}

View File

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

View File

@ -118,7 +118,6 @@ public class ReturnYouTubeDislike {
*/ */
public static final int leftSeparatorShapePaddingPixels; public static final int leftSeparatorShapePaddingPixels;
private static final ShapeDrawable leftSeparatorShape; private static final ShapeDrawable leftSeparatorShape;
public static final Locale locale;
static { static {
final Resources resources = Utils.getResources(); final Resources resources = Utils.getResources();
@ -135,7 +134,6 @@ public class ReturnYouTubeDislike {
leftSeparatorShape = new ShapeDrawable(new RectShape()); leftSeparatorShape = new ShapeDrawable(new RectShape());
leftSeparatorShape.setBounds(leftSeparatorBounds); leftSeparatorShape.setBounds(leftSeparatorBounds);
locale = resources.getConfiguration().getLocales().get(0);
ReturnYouTubeDislikeApi.toastOnConnectionError = Settings.RYD_TOAST_ON_CONNECTION_ERROR.get(); ReturnYouTubeDislikeApi.toastOnConnectionError = Settings.RYD_TOAST_ON_CONNECTION_ERROR.get();
} }

View File

@ -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.MiniplayerPatch;
import app.revanced.extension.youtube.patches.general.YouTubeMusicActionsPatch; import app.revanced.extension.youtube.patches.general.YouTubeMusicActionsPatch;
import app.revanced.extension.youtube.patches.misc.WatchHistoryPatch.WatchHistoryType; 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.AnimationFeedbackPatch.AnimationType;
import app.revanced.extension.youtube.patches.shorts.ShortsRepeatStatePatch.ShortsLoopBehavior; import app.revanced.extension.youtube.patches.shorts.ShortsRepeatStatePatch.ShortsLoopBehavior;
import app.revanced.extension.youtube.patches.utils.PatchStatus; import app.revanced.extension.youtube.patches.utils.PatchStatus;
@ -147,7 +148,7 @@ public class Settings extends BaseSettings {
new ChangeStartPagePatch.ChangeStartPageTypeAvailability()); new ChangeStartPagePatch.ChangeStartPageTypeAvailability());
public static final BooleanSetting DISABLE_AUTO_AUDIO_TRACKS = new BooleanSetting("revanced_disable_auto_audio_tracks", FALSE); 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_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 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_FLOATING_MICROPHONE = new BooleanSetting("revanced_hide_floating_microphone", TRUE, true);
public static final BooleanSetting HIDE_GRAY_SEPARATOR = new BooleanSetting("revanced_hide_gray_separator", 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_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 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 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 ENABLE_TRANSLUCENT_NAVIGATION_BAR = new BooleanSetting("revanced_enable_translucent_navigation_bar", 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 HIDE_NAVIGATION_BAR = new BooleanSetting("revanced_hide_navigation_bar", FALSE, true); public static final BooleanSetting HIDE_NAVIGATION_BAR = new BooleanSetting("revanced_hide_navigation_bar", FALSE, true);
// PreferenceScreen: General - Override buttons // PreferenceScreen: General - Override buttons
@ -336,6 +336,8 @@ public class Settings extends BaseSettings {
// PreferenceScreen: Player - Fullscreen // PreferenceScreen: Player - Fullscreen
public static final BooleanSetting DISABLE_ENGAGEMENT_PANEL = new BooleanSetting("revanced_disable_engagement_panel", FALSE, true); 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<FullscreenMode> 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 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_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); 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 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 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 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); 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+ @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_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_SWIPE_TO_SWITCH_VIDEO = new BooleanSetting("revanced_disable_swipe_to_switch_video", TRUE, 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_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 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); 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_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 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_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 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 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)); 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); public static final BooleanSetting RESTORE_OLD_VIDEO_QUALITY_MENU = new BooleanSetting("revanced_restore_old_video_quality_menu", TRUE, true);
// Experimental Flags // 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 = 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 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 = 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); public static final BooleanSetting SKIP_PRELOADED_BUFFER_TOAST = new BooleanSetting("revanced_skip_preloaded_buffer_toast", TRUE);

View File

@ -1,5 +1,6 @@
package app.revanced.extension.youtube.settings.preference; 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.showRestartDialog;
import static app.revanced.extension.shared.settings.preference.AbstractPreferenceFragment.updateListPreferenceSummary; import static app.revanced.extension.shared.settings.preference.AbstractPreferenceFragment.updateListPreferenceSummary;
import static app.revanced.extension.shared.utils.ResourceUtils.getXmlIdentifier; import static app.revanced.extension.shared.utils.ResourceUtils.getXmlIdentifier;
@ -18,6 +19,7 @@ import android.app.Dialog;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.graphics.Insets;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.preference.EditTextPreference; import android.preference.EditTextPreference;
@ -31,6 +33,7 @@ import android.preference.PreferenceScreen;
import android.preference.SwitchPreference; import android.preference.SwitchPreference;
import android.util.TypedValue; import android.util.TypedValue;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.WindowInsets;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toolbar; import android.widget.Toolbar;
@ -202,6 +205,15 @@ public class ReVancedPreferenceFragment extends PreferenceFragment {
.findViewById(android.R.id.content) .findViewById(android.R.id.content)
.getParent(); .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 toolbar = new Toolbar(preferenceScreen.getContext());
toolbar.setTitle(preferenceScreen.getTitle()); toolbar.setTitle(preferenceScreen.getTitle());
@ -219,6 +231,8 @@ public class ReVancedPreferenceFragment extends PreferenceFragment {
toolbarTextView.setTextColor(ThemeUtils.getForegroundColor()); toolbarTextView.setTextColor(ThemeUtils.getForegroundColor());
} }
setToolbarLayoutParams(toolbar);
rootView.addView(toolbar, 0); rootView.addView(toolbar, 0);
return false; return false;
} }

View File

@ -46,10 +46,8 @@ public class ReVancedSettingsPreference extends ReVancedPreferenceFragment {
enableDisablePreferences(); enableDisablePreferences();
AmbientModePreferenceLinks(); AmbientModePreferenceLinks();
ChangeHeaderPreferenceLinks();
ExternalDownloaderPreferenceLinks(); ExternalDownloaderPreferenceLinks();
FullScreenPanelPreferenceLinks(); FullScreenPanelPreferenceLinks();
LayoutOverrideLinks();
MiniPlayerPreferenceLinks(); MiniPlayerPreferenceLinks();
NavigationPreferenceLinks(); NavigationPreferenceLinks();
RYDPreferenceLinks(); 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 * 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 * 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_SAVE_TO_PLAYLIST_BUTTON,
Settings.HIDE_QUICK_ACTIONS_SHARE_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 Settings.REPLACE_TOOLBAR_CREATE_BUTTON_TYPE
); );
enableDisablePreferences( enableDisablePreferences(
!isSDKAbove(33), !isSDKAbove(31),
Settings.DISABLE_TRANSLUCENT_NAVIGATION_BAR_LIGHT, Settings.ENABLE_TRANSLUCENT_NAVIGATION_BAR
Settings.DISABLE_TRANSLUCENT_NAVIGATION_BAR_DARK
); );
} }

View File

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

View File

@ -63,13 +63,7 @@ public class SpoofStreamingDataSideEffectsPreference extends Preference {
private void updateUI() { private void updateUI() {
final String clientName = Settings.SPOOF_STREAMING_DATA_TYPE.get().name().toLowerCase(); final String clientName = Settings.SPOOF_STREAMING_DATA_TYPE.get().name().toLowerCase();
String summaryTextKey = "revanced_spoof_streaming_data_side_effects_"; final String summaryTextKey = "revanced_spoof_streaming_data_side_effects_" + clientName;
if (Settings.SPOOF_STREAMING_DATA_ANDROID_ONLY.get()) {
summaryTextKey += "android";
} else {
summaryTextKey += clientName;
}
setSummary(str(summaryTextKey)); setSummary(str(summaryTextKey));
setEnabled(Settings.SPOOF_STREAMING_DATA.get()); setEnabled(Settings.SPOOF_STREAMING_DATA.get());

View File

@ -31,7 +31,7 @@ enum class BottomSheetState {
onChange(currentBottomSheetState) onChange(currentBottomSheetState)
} }
@Volatile // value is read/write from different threads @Volatile // Read/write from different threads.
private var currentBottomSheetState = CLOSED private var currentBottomSheetState = CLOSED
/** /**

View File

@ -40,7 +40,7 @@ enum class LockModeState {
onChange(value) onChange(value)
} }
@Volatile // value is read/write from different threads @Volatile // Read/write from different threads.
private var currentLockModeState = LOCK_MODE_STATE_ENUM_UNKNOWN private var currentLockModeState = LOCK_MODE_STATE_ENUM_UNKNOWN
/** /**

View File

@ -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 * 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. * be as small as possible while still allowing enough time for the nav bar to update.
* <p> * <p>
* YT calls it's back button handlers out of order, * YT calls it's back button handlers out of order, and litho starts filtering before the
* and litho starts filtering before the navigation bar is updated. * 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.
* <p> * <p>
* Fixing this situation and not needlessly waiting requires somehow * On average the time between pressing the back button and the first litho event is
* detecting if a back button key-press will cause a tab change. * 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.
* <p> * <p>
* Typically after pressing the back button, the time between the first litho event and * This delay is only noticeable when the device back button/gesture will not
* when the nav button is updated is about 10-20ms. Using 50-100ms here should be enough time * change the current navigation tab, such as backing out of the watch history.
* and not noticeable, since YT typically takes 100-200ms (or more) to update the view anyways.
* <p> * <p>
* This issue can also be avoided on a patch by patch basis, by avoiding calls to * This issue can also be avoided on a patch by patch basis, by avoiding calls to
* {@link NavigationButton#getSelectedNavigationButton()} unless absolutely necessary. * {@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. * 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. // 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. // 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. // 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; return;
} }
@ -102,7 +104,9 @@ public final class NavigationBar {
Logger.printDebug(() -> "Latch wait timed out"); Logger.printDebug(() -> "Latch wait timed out");
} catch (InterruptedException ex) { } 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 {
* <p> * <p>
* All code calling this method should handle a null return value. * All code calling this method should handle a null return value.
* <p> * <p>
* <b>Due to issues with how YT processes physical back button events, * <b>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 75ms * this patch uses workarounds that can cause this method to take up to 120ms
* if the device back button was recently pressed.</b> * if the device back button was recently pressed.</b>
* *
* @return The active navigation tab. * @return The active navigation tab.

View File

@ -14,7 +14,7 @@ enum class PlayerControlsVisibility {
companion object { companion object {
private val nameToPlayerControlsVisibility = values().associateBy { it.name } private val nameToPlayerControlsVisibility = entries.associateBy { it.name }
@JvmStatic @JvmStatic
fun setFromString(enumName: String) { fun setFromString(enumName: String) {
@ -38,6 +38,7 @@ enum class PlayerControlsVisibility {
currentPlayerControlsVisibility = value currentPlayerControlsVisibility = value
} }
@Volatile // Read/write from different threads.
private var currentPlayerControlsVisibility: PlayerControlsVisibility? = null private var currentPlayerControlsVisibility: PlayerControlsVisibility? = null
} }
} }

View File

@ -71,7 +71,7 @@ enum class PlayerType {
onChange(value) onChange(value)
} }
@Volatile // value is read/write from different threads @Volatile // Read/write from different threads.
private var currentPlayerType = NONE private var currentPlayerType = NONE
/** /**

View File

@ -31,7 +31,7 @@ enum class ShortsPlayerState {
onChange(value) onChange(value)
} }
@Volatile // value is read/write from different threads @Volatile // Read/write from different threads.
private var currentShortsPlayerState = CLOSED private var currentShortsPlayerState = CLOSED
/** /**

View File

@ -39,6 +39,7 @@ enum class VideoState {
currentVideoState = value currentVideoState = value
} }
@Volatile // Read/write from different threads.
private var currentVideoState: VideoState? = null private var currentVideoState: VideoState? = null
} }
} }

View File

@ -126,8 +126,12 @@ public class SBRequester {
HttpURLConnection connection = getConnectionFromRoute(SBRoutes.SUBMIT_SEGMENTS, privateUserId, videoId, category, start, end, duration); HttpURLConnection connection = getConnectionFromRoute(SBRoutes.SUBMIT_SEGMENTS, privateUserId, videoId, category, start, end, duration);
final int responseCode = connection.getResponseCode(); final int responseCode = connection.getResponseCode();
String userMessage = switch (responseCode) { if (responseCode == HTTP_STATUS_CODE_SUCCESS) {
case HTTP_STATUS_CODE_SUCCESS -> str("revanced_sb_submit_succeeded"); Utils.showToastLong(str("revanced_sb_submit_succeeded"));
return;
}
String userErrorMessage = switch (responseCode) {
case 409 -> str("revanced_sb_submit_failed_duplicate"); case 409 -> str("revanced_sb_submit_failed_duplicate");
case 403 -> str("revanced_sb_submit_failed_forbidden", case 403 -> str("revanced_sb_submit_failed_forbidden",
Requester.parseErrorStringAndDisconnect(connection)); Requester.parseErrorStringAndDisconnect(connection));
@ -137,9 +141,10 @@ public class SBRequester {
default -> str("revanced_sb_submit_failed_unknown_error", default -> str("revanced_sb_submit_failed_unknown_error",
responseCode, connection.getResponseMessage()); responseCode, connection.getResponseMessage());
}; };
// Message might be about the users account or an error too large to show in a toast. // Message might be about the users account or an error too large to show in a toast.
// Use a dialog instead. // Use a dialog instead.
SponsorBlockUtils.showErrorDialog(userMessage); SponsorBlockUtils.showErrorDialog(userErrorMessage);
} catch (SocketTimeoutException ex) { } catch (SocketTimeoutException ex) {
Logger.printDebug(() -> "Timeout", ex); Logger.printDebug(() -> "Timeout", ex);
Utils.showToastLong(str("revanced_sb_submit_failed_timeout")); Utils.showToastLong(str("revanced_sb_submit_failed_timeout"));

View File

@ -87,8 +87,10 @@ abstract class BaseGestureController(
distanceX: Float, distanceX: Float,
distanceY: Float, distanceY: Float,
): Boolean { ): Boolean {
if (from == null) return false
// submit to swipe detector // submit to swipe detector
submitForSwipe(from!!, to, distanceX, distanceY) submitForSwipe(from, to, distanceX, distanceY)
// call swipe callback if in a swipe // call swipe callback if in a swipe
return if (currentSwipe != SwipeDetector.SwipeDirection.NONE) { return if (currentSwipe != SwipeDetector.SwipeDirection.NONE) {

View File

@ -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_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_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_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_29_OR_GREATER = getAppVersionName().compareTo("19.29.00") >= 0;
public static final boolean IS_19_34_OR_GREATER = getAppVersionName().compareTo("19.34.00") >= 0; public static final boolean IS_19_34_OR_GREATER = getAppVersionName().compareTo("19.34.00") >= 0;

View File

@ -242,6 +242,20 @@ public class VideoUtils extends IntentUtils {
return !isExternalDownloaderLaunched.get() && original; 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. * Rest of the implementation added by patch.
*/ */

View File

@ -4,7 +4,6 @@ import android.app.Activity;
import android.content.Context; import android.content.Context;
import android.os.Bundle; import android.os.Bundle;
import android.util.TypedValue; import android.util.TypedValue;
import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.EditText; import android.widget.EditText;
import android.widget.SearchView; import android.widget.SearchView;
@ -88,17 +87,21 @@ public class VideoQualitySettingsActivity extends Activity {
fragment.filterPreferences(query); fragment.filterPreferences(query);
} }
private static ViewGroup.LayoutParams lp;
public static void setToolbarLayoutParams(Toolbar toolbar) {
if (lp != null) {
toolbar.setLayoutParams(lp);
}
}
private void setToolbar() { private void setToolbar() {
if (!(findViewById(ResourceUtils.getIdIdentifier("revanced_toolbar_parent")) instanceof ViewGroup toolBarParent)) ViewGroup toolBarParent = findViewById(ResourceUtils.getIdIdentifier("revanced_toolbar_parent"));
return;
// Remove dummy toolbar. // Remove dummy toolbar.
for (int i = 0; i < toolBarParent.getChildCount(); i++) { ViewGroup dummyToolbar = toolBarParent.findViewById(ResourceUtils.getIdIdentifier("revanced_toolbar"));
View view = toolBarParent.getChildAt(i); lp = dummyToolbar.getLayoutParams();
if (view != null) { toolBarParent.removeView(dummyToolbar);
toolBarParent.removeView(view);
}
}
Toolbar toolbar = new Toolbar(toolBarParent.getContext()); Toolbar toolbar = new Toolbar(toolBarParent.getContext());
toolbar.setBackgroundColor(ThemeUtils.getToolbarBackgroundColor()); toolbar.setBackgroundColor(ThemeUtils.getToolbarBackgroundColor());
@ -112,6 +115,7 @@ public class VideoQualitySettingsActivity extends Activity {
if (toolbarTextView != null) { if (toolbarTextView != null) {
toolbarTextView.setTextColor(ThemeUtils.getForegroundColor()); toolbarTextView.setTextColor(ThemeUtils.getForegroundColor());
} }
setToolbarLayoutParams(toolbar);
toolBarParent.addView(toolbar, 0); toolBarParent.addView(toolbar, 0);
} }

View File

@ -4,5 +4,5 @@ org.gradle.parallel = true
android.useAndroidX = true android.useAndroidX = true
kotlin.code.style = official kotlin.code.style = official
kotlin.jvm.target.validation.mode = IGNORE kotlin.jvm.target.validation.mode = IGNORE
version = 5.1.3 version = 5.2.1

View File

@ -1,7 +1,7 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionSha256Sum=31c55713e40233a8303827ceb42ca48a47267a0ad4bab9177123121e71524c26 distributionSha256Sum=7a00d51fb93147819aab76024feece20b6b84e420694101f276be952e08bef03
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

3
gradlew vendored
View File

@ -86,8 +86,7 @@ done
# shellcheck disable=SC2034 # shellcheck disable=SC2034
APP_BASE_NAME=${0##*/} APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) # 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 APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value. # Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum MAX_FD=maximum

View File

@ -43,26 +43,6 @@
}, },
"options": [] "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", "name": "Bitrate default value",
"description": "Sets the audio quality to \u0027Always High\u0027 when you first install the app.", "description": "Sets the audio quality to \u0027Always High\u0027 when you first install the app.",
@ -186,7 +166,7 @@
}, },
{ {
"name": "Change share sheet", "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, "use": true,
"dependencies": [ "dependencies": [
"Settings for YouTube Music", "Settings for YouTube Music",
@ -207,7 +187,7 @@
}, },
{ {
"name": "Change share sheet", "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, "use": true,
"dependencies": [ "dependencies": [
"Settings for YouTube", "Settings for YouTube",
@ -364,7 +344,7 @@
{ {
"key": "changeSplashIcon", "key": "changeSplashIcon",
"title": "Change splash icons", "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, "required": true,
"type": "kotlin.Boolean", "type": "kotlin.Boolean",
"default": true, "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", "name": "Description components",
"description": "Adds options to hide and disable description components.", "description": "Adds options to hide and disable description components.",
@ -819,6 +846,27 @@
}, },
"options": [] "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", "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.", "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", "Settings for YouTube",
"BytecodePatch", "BytecodePatch",
"BytecodePatch", "BytecodePatch",
"BytecodePatch",
"BytecodePatch",
"BytecodePatch",
"ResourcePatch", "ResourcePatch",
"ResourcePatch" "ResourcePatch"
], ],
@ -1493,7 +1544,8 @@
"BytecodePatch", "BytecodePatch",
"ResourcePatch", "ResourcePatch",
"BytecodePatch", "BytecodePatch",
"BytecodePatch" "BytecodePatch",
"ResourcePatch"
], ],
"compatiblePackages": { "compatiblePackages": {
"com.google.android.youtube": [ "com.google.android.youtube": [
@ -1619,7 +1671,7 @@
}, },
{ {
"name": "Hide shortcuts", "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, "use": false,
"dependencies": [ "dependencies": [
"Settings for YouTube", "Settings for YouTube",
@ -1861,7 +1913,8 @@
"BytecodePatch", "BytecodePatch",
"ResourcePatch", "ResourcePatch",
"ResourcePatch", "ResourcePatch",
"Settings for YouTube" "Settings for YouTube",
"ResourcePatch"
], ],
"compatiblePackages": { "compatiblePackages": {
"com.google.android.youtube": [ "com.google.android.youtube": [
@ -1891,7 +1944,7 @@
{ {
"key": "bottomMargin", "key": "bottomMargin",
"title": "Bottom margin", "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, "required": true,
"type": "kotlin.String", "type": "kotlin.String",
"default": "2.5dip", "default": "2.5dip",
@ -1904,7 +1957,7 @@
{ {
"key": "widerButtonsSpace", "key": "widerButtonsSpace",
"title": "Wider between-buttons space", "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, "required": true,
"type": "kotlin.Boolean", "type": "kotlin.Boolean",
"default": false, "default": false,
@ -2530,7 +2583,8 @@
"use": true, "use": true,
"dependencies": [ "dependencies": [
"BytecodePatch", "BytecodePatch",
"Settings for YouTube Music" "Settings for YouTube Music",
"BytecodePatch"
], ],
"compatiblePackages": { "compatiblePackages": {
"com.google.android.apps.youtube.music": [ "com.google.android.apps.youtube.music": [
@ -2651,7 +2705,7 @@
}, },
{ {
"name": "Toolbar components", "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, "use": true,
"dependencies": [ "dependencies": [
"BytecodePatch", "BytecodePatch",

View File

@ -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 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 final class app/revanced/patches/music/general/autocaptions/AutoCaptionsPatchKt {
public static final fun getAutoCaptionsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; 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 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 final class app/revanced/patches/music/layout/translations/TranslationsPatchKt {
public static final fun getTranslationsPatch ()Lapp/revanced/patcher/patch/ResourcePatch; 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 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 final class app/revanced/patches/music/misc/backgroundplayback/BackgroundPlaybackPatchKt {
public static final fun getBackgroundPlaybackPatch ()Lapp/revanced/patcher/patch/BytecodePatch; 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 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 final class app/revanced/patches/music/utils/extension/SharedExtensionPatchKt {
public static final fun getSharedExtensionPatch ()Lapp/revanced/patcher/patch/BytecodePatch; 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 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; 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_27_or_greater ()Z
public static final fun is_6_36_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_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_06_or_greater ()Z
public static final fun is_7_13_or_greater ()Z public static final fun is_7_13_or_greater ()Z
public static final fun is_7_17_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 final class app/revanced/patches/music/utils/resourceid/SharedResourceIdPatchKt {
public static final fun getAccountSwitcherAccessibility ()J 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 getBottomSheetRecyclerView ()J
public static final fun getButtonContainer ()J public static final fun getButtonContainer ()J
public static final fun getButtonIconPaddingMedium ()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 getColorGrey ()J
public static final fun getDarkBackground ()J public static final fun getDarkBackground ()J
public static final fun getDesignBottomSheetDialog ()J public static final fun getDesignBottomSheetDialog ()J
public static final fun getElementsContainer ()J
public static final fun getEndButtonsContainer ()J public static final fun getEndButtonsContainer ()J
public static final fun getFloatingLayout ()J public static final fun getFloatingLayout ()J
public static final fun getHistoryMenuItem ()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 getTouchOutside ()J
public static final fun getTrimSilenceSwitch ()J public static final fun getTrimSilenceSwitch ()J
public static final fun getVarispeedUnavailableTitle ()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 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 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 final class app/revanced/patches/reddit/ad/AdsPatchKt {
public static final fun getAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; 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 getMainActivityMutableClass ()Lapp/revanced/patcher/util/proxy/mutableTypes/MutableClass;
public static final fun getOnConfigurationChangedMethod ()Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod; 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 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 { 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 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 final class app/revanced/patches/shared/opus/BaseOpusCodecsPatchKt {
public static final fun baseOpusCodecsPatch (Ljava/lang/String;)Lapp/revanced/patcher/patch/BytecodePatch; 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 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 final class app/revanced/patches/youtube/utils/gms/GmsCoreSupportPatchKt {
public static final fun getGmsCoreSupportPatch ()Lapp/revanced/patcher/patch/BytecodePatch; 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;I)I
public static final fun or (Lcom/android/tools/smali/dexlib2/AccessFlags;Lcom/android/tools/smali/dexlib2/AccessFlags;)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 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 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 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 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 (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 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 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 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 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 public static final fun insertNode (Lorg/w3c/dom/Node;Ljava/lang/String;Lorg/w3c/dom/Node;Lkotlin/jvm/functions/Function1;)V

View File

@ -30,7 +30,9 @@ internal class JsonPatchesFileGenerator : PatchesFileGenerator {
}, },
) )
}.let { }.let {
patchesJson.writeText(GsonBuilder().serializeNulls().setPrettyPrinting().create().toJson(it)) patchesJson.writeText(
GsonBuilder().serializeNulls().setPrettyPrinting().create().toJson(it)
)
} }
} }

View File

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

View File

@ -5,6 +5,13 @@ import app.revanced.patcher.patch.resourcePatch
import app.revanced.patcher.patch.stringOption import app.revanced.patcher.patch.stringOption
import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE 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.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.getIconType
import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus
import app.revanced.patches.music.utils.settings.settingsPatch 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.Utils.trimIndentMultiline
import app.revanced.util.copyFile import app.revanced.util.copyFile
import app.revanced.util.copyResources import app.revanced.util.copyResources
import app.revanced.util.fingerprint.injectLiteralInstructionBooleanCall import app.revanced.util.replaceLiteralInstructionCall
import app.revanced.util.fingerprint.resolvable
import app.revanced.util.underBarOrThrow import app.revanced.util.underBarOrThrow
import app.revanced.util.valueOrThrow import app.revanced.util.valueOrThrow
@ -100,7 +106,13 @@ private val getDescription = {
private val changeHeaderBytecodePatch = bytecodePatch( private val changeHeaderBytecodePatch = bytecodePatch(
description = "changeHeaderBytecodePatch" description = "changeHeaderBytecodePatch"
) { ) {
dependsOn(
sharedResourceIdPatch,
versionCheckPatch,
)
execute { execute {
/** /**
* New Header has been added from YouTube Music v7.04.51. * New Header has been added from YouTube Music v7.04.51.
* *
@ -108,16 +120,17 @@ private val changeHeaderBytecodePatch = bytecodePatch(
* The only difference between the existing header and the new header is the dimensions of the image. * The only difference between the existing header and the new header is the dimensions of the image.
* *
* The affected patch is [changeHeaderPatch]. * 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 return@execute
} }
headerSwitchConfigFingerprint.injectLiteralInstructionBooleanCall(
45617851L, listOf(
"0x0" actionBarLogoRingo2 to actionBarLogo,
) ytmLogoRingo2 to ytmLogo,
).forEach { (originalResource, replacementResource) ->
replaceLiteralInstructionCall(originalResource, replacementResource)
}
} }
} }

View File

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

View File

@ -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<OneRegisterInstruction>(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)
}
}

View File

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

View File

@ -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<FiveRegisterInstruction>(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<MethodReference>()?.name == "<init>"
}
val onClickListenerSyntheticClass =
(getInstruction<ReferenceInstruction>(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)
}
}

View File

@ -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<MethodReference>()?.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.")
)

View File

@ -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.playerViewPager
import app.revanced.patches.music.utils.resourceid.remixGenericButtonSize import app.revanced.patches.music.utils.resourceid.remixGenericButtonSize
import app.revanced.patches.music.utils.resourceid.tapBloomView import app.revanced.patches.music.utils.resourceid.tapBloomView
import app.revanced.util.containsLiteralInstruction
import app.revanced.util.fingerprint.legacyFingerprint import app.revanced.util.fingerprint.legacyFingerprint
import app.revanced.util.getReference import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstruction import app.revanced.util.indexOfFirstInstruction
@ -50,6 +51,7 @@ internal val engagementPanelHeightFingerprint = legacyFingerprint(
), ),
parameters = emptyList(), parameters = emptyList(),
customFingerprint = { method, _ -> customFingerprint = { method, _ ->
method.containsLiteralInstruction(1) &&
method.indexOfFirstInstruction { method.indexOfFirstInstruction {
opcode == Opcode.INVOKE_VIRTUAL && opcode == Opcode.INVOKE_VIRTUAL &&
getReference<MethodReference>()?.name == "booleanValue" getReference<MethodReference>()?.name == "booleanValue"

View File

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

View File

@ -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<MethodReference>()?.definingClass?.endsWith("/MppWatchWhileLayout;") == true
}

View File

@ -1,5 +1,6 @@
package app.revanced.patches.music.utils.fix.streamingdata 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.COMPATIBLE_PACKAGE
import app.revanced.patches.music.utils.compatibility.Constants.YOUTUBE_MUSIC_PACKAGE_NAME import app.revanced.patches.music.utils.compatibility.Constants.YOUTUBE_MUSIC_PACKAGE_NAME
import app.revanced.patches.music.utils.patch.PatchList.SPOOF_STREAMING_DATA 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.addPreferenceWithIntent
import app.revanced.patches.music.utils.settings.addSwitchPreference import app.revanced.patches.music.utils.settings.addSwitchPreference
import app.revanced.patches.music.utils.settings.settingsPatch 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.streamingdata.baseSpoofStreamingDataPatch
import app.revanced.patches.shared.spoof.useragent.baseSpoofUserAgentPatch import app.revanced.patches.shared.spoof.useragent.baseSpoofUserAgentPatch
import app.revanced.util.findMethodOrThrow
const val EXTENSION_CLASS_DESCRIPTOR =
"$SPOOF_PATH/SpoofStreamingDataPatch;"
@Suppress("unused") @Suppress("unused")
val spoofStreamingDataPatch = baseSpoofStreamingDataPatch( val spoofStreamingDataPatch = baseSpoofStreamingDataPatch(
@ -19,9 +28,22 @@ val spoofStreamingDataPatch = baseSpoofStreamingDataPatch(
dependsOn( dependsOn(
baseSpoofUserAgentPatch(YOUTUBE_MUSIC_PACKAGE_NAME), baseSpoofUserAgentPatch(YOUTUBE_MUSIC_PACKAGE_NAME),
settingsPatch, 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( addSwitchPreference(
CategoryType.MISC, CategoryType.MISC,
"revanced_spoof_streaming_data", "revanced_spoof_streaming_data",

View File

@ -5,10 +5,6 @@ internal enum class PatchList(
val summary: String, val summary: String,
var included: Boolean? = false var included: Boolean? = false
) { ) {
AMOLED(
"Amoled",
"Applies a pure black theme to some components."
),
BITRATE_DEFAULT_VALUE( BITRATE_DEFAULT_VALUE(
"Bitrate default value", "Bitrate default value",
"Sets the audio quality to 'Always High' when you first install the app." "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(
"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(
"Change start page", "Change start page",
@ -41,6 +37,10 @@ internal enum class PatchList(
"Custom header for YouTube Music", "Custom header for YouTube Music",
"Applies a custom header in the top left corner within the app." "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(
"Disable Cairo splash animation", "Disable Cairo splash animation",
"Adds an option to disable Cairo splash animation." "Adds an option to disable Cairo splash animation."
@ -57,6 +57,10 @@ internal enum class PatchList(
"Disable dislike redirection", "Disable dislike redirection",
"Adds an option to disable redirection to the next track when clicking the Dislike button." "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(
"Enable OPUS codec", "Enable OPUS codec",
"Adds an options to enable the OPUS audio codec if the player response includes." "Adds an options to enable the OPUS audio codec if the player response includes."

View File

@ -11,6 +11,8 @@ var is_6_36_or_greater = false
private set private set
var is_6_42_or_greater = false var is_6_42_or_greater = false
private set private set
var is_7_03_or_greater = false
private set
var is_7_06_or_greater = false var is_7_06_or_greater = false
private set private set
var is_7_13_or_greater = false var is_7_13_or_greater = false
@ -43,6 +45,7 @@ val versionCheckPatch = resourcePatch(
is_6_27_or_greater = 234412000 <= playStoreServicesVersion is_6_27_or_greater = 234412000 <= playStoreServicesVersion
is_6_36_or_greater = 240399000 <= playStoreServicesVersion is_6_36_or_greater = 240399000 <= playStoreServicesVersion
is_6_42_or_greater = 240999000 <= playStoreServicesVersion is_6_42_or_greater = 240999000 <= playStoreServicesVersion
is_7_03_or_greater = 242199000 <= playStoreServicesVersion
is_7_06_or_greater = 242499000 <= playStoreServicesVersion is_7_06_or_greater = 242499000 <= playStoreServicesVersion
is_7_13_or_greater = 243199000 <= playStoreServicesVersion is_7_13_or_greater = 243199000 <= playStoreServicesVersion
is_7_17_or_greater = 243530000 <= playStoreServicesVersion is_7_17_or_greater = 243530000 <= playStoreServicesVersion

View File

@ -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.BOOL
import app.revanced.patches.shared.mapping.ResourceType.COLOR import app.revanced.patches.shared.mapping.ResourceType.COLOR
import app.revanced.patches.shared.mapping.ResourceType.DIMEN 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.ID
import app.revanced.patches.shared.mapping.ResourceType.LAYOUT import app.revanced.patches.shared.mapping.ResourceType.LAYOUT
import app.revanced.patches.shared.mapping.ResourceType.STRING import app.revanced.patches.shared.mapping.ResourceType.STRING
@ -14,6 +15,10 @@ import app.revanced.patches.shared.mapping.resourceMappings
var accountSwitcherAccessibility = -1L var accountSwitcherAccessibility = -1L
private set private set
var actionBarLogo = -1L
private set
var actionBarLogoRingo2 = -1L
private set
var bottomSheetRecyclerView = -1L var bottomSheetRecyclerView = -1L
private set private set
var buttonContainer = -1L var buttonContainer = -1L
@ -28,6 +33,8 @@ var darkBackground = -1L
private set private set
var designBottomSheetDialog = -1L var designBottomSheetDialog = -1L
private set private set
var elementsContainer = -1L
private set
var endButtonsContainer = -1L var endButtonsContainer = -1L
private set private set
var floatingLayout = -1L var floatingLayout = -1L
@ -94,6 +101,10 @@ var trimSilenceSwitch = -1L
private set private set
var varispeedUnavailableTitle = -1L var varispeedUnavailableTitle = -1L
private set private set
var ytmLogo = -1L
private set
var ytmLogoRingo2 = -1L
private set
internal val sharedResourceIdPatch = resourcePatch( internal val sharedResourceIdPatch = resourcePatch(
description = "sharedResourceIdPatch" description = "sharedResourceIdPatch"
@ -105,6 +116,14 @@ internal val sharedResourceIdPatch = resourcePatch(
STRING, STRING,
"account_switcher_accessibility_label", "account_switcher_accessibility_label",
] ]
actionBarLogo = resourceMappings[
DRAWABLE,
"action_bar_logo",
]
actionBarLogoRingo2 = resourceMappings[
DRAWABLE,
"action_bar_logo_ringo2",
]
bottomSheetRecyclerView = resourceMappings[ bottomSheetRecyclerView = resourceMappings[
LAYOUT, LAYOUT,
"bottom_sheet_recycler_view" "bottom_sheet_recycler_view"
@ -133,6 +152,10 @@ internal val sharedResourceIdPatch = resourcePatch(
LAYOUT, LAYOUT,
"design_bottom_sheet_dialog" "design_bottom_sheet_dialog"
] ]
elementsContainer = resourceMappings[
ID,
"elements_container"
]
endButtonsContainer = resourceMappings[ endButtonsContainer = resourceMappings[
ID, ID,
"end_buttons_container" "end_buttons_container"
@ -265,5 +288,13 @@ internal val sharedResourceIdPatch = resourcePatch(
STRING, STRING,
"varispeed_unavailable_title" "varispeed_unavailable_title"
] ]
ytmLogo = resourceMappings[
DRAWABLE,
"ytm_logo",
]
ytmLogoRingo2 = resourceMappings[
DRAWABLE,
"ytm_logo_ringo2",
]
} }
} }

View File

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

View File

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

View File

@ -10,7 +10,6 @@ import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
import app.revanced.patcher.util.smali.ExternalLabel import app.revanced.patcher.util.smali.ExternalLabel
import app.revanced.patches.shared.extension.Constants.PATCHES_PATH 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.fingerprint.methodOrThrow
import app.revanced.util.getReference import app.revanced.util.getReference
import app.revanced.util.getWalkerMethod import app.revanced.util.getWalkerMethod
@ -33,11 +32,7 @@ fun baseAdsPatch(
) { ) {
execute { execute {
setOf( videoAdsFingerprint.methodOrThrow().apply {
sslGuardFingerprint,
videoAdsFingerprint,
).forEach { fingerprint ->
fingerprint.methodOrThrow().apply {
addInstructionsWithLabels( addInstructionsWithLabels(
0, """ 0, """
invoke-static {}, $classDescriptor->$methodDescriptor()Z invoke-static {}, $classDescriptor->$methodDescriptor()Z
@ -47,7 +42,6 @@ fun baseAdsPatch(
""", ExternalLabel("show_ads", getInstruction(0)) """, ExternalLabel("show_ads", getInstruction(0))
) )
} }
}
musicAdsFingerprint.methodOrThrow().apply { musicAdsFingerprint.methodOrThrow().apply {
val targetIndex = indexOfFirstInstructionOrThrow { val targetIndex = indexOfFirstInstructionOrThrow {
@ -67,21 +61,6 @@ fun baseAdsPatch(
) )
} }
advertisingIdFingerprint.matchOrThrow().let {
it.method.apply {
val insertIndex = it.stringMatches!!.first().index
val insertRegister = getInstruction<OneRegisterInstruction>(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))
)
}
}
} }
} }

View File

@ -4,24 +4,6 @@ import app.revanced.util.fingerprint.legacyFingerprint
import app.revanced.util.or import app.revanced.util.or
import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.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( internal val musicAdsFingerprint = legacyFingerprint(
name = "musicAdsFingerprint", name = "musicAdsFingerprint",

View File

@ -5,11 +5,8 @@ import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstruction import app.revanced.util.indexOfFirstInstruction
import app.revanced.util.or import app.revanced.util.or
import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.Method 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.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" const val GET_GMS_CORE_VENDOR_GROUP_ID_METHOD_NAME = "getGmsCoreVendorGroupId"
@ -25,15 +22,6 @@ internal val castContextFetchFingerprint = legacyFingerprint(
strings = listOf("Error fetching CastContext.") 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( internal val googlePlayUtilityFingerprint = legacyFingerprint(
name = "castContextFetchFingerprint", name = "castContextFetchFingerprint",
returnType = "I", returnType = "I",
@ -53,42 +41,9 @@ internal val serviceCheckFingerprint = legacyFingerprint(
strings = listOf("Google Play Services not available") strings = listOf("Google Play Services not available")
) )
internal val primesApiFingerprint = legacyFingerprint( internal val primeMethodFingerprint = legacyFingerprint(
name = "primesApiFingerprint", name = "primeMethodFingerprint",
returnType = "V", strings = listOf("com.google.android.GoogleCamera", "com.android.vending")
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<StringReference>()
?.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<StringReference>()
?.string.toString()
.startsWith("Primes did not observe lifecycle events in the expected order.")
} >= 0
}
) )
internal val certificateFingerprint = legacyFingerprint( internal val certificateFingerprint = legacyFingerprint(

View File

@ -1,9 +1,9 @@
package app.revanced.patches.shared.gms package app.revanced.patches.shared.gms
import app.revanced.patcher.Fingerprint 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.addInstructions
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction 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.extensions.InstructionExtensions.replaceInstruction
import app.revanced.patcher.patch.BytecodePatchBuilder import app.revanced.patcher.patch.BytecodePatchBuilder
import app.revanced.patcher.patch.BytecodePatchContext 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.fingerprint.mutableClassOrThrow
import app.revanced.util.getReference import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstruction import app.revanced.util.indexOfFirstInstruction
import app.revanced.util.indexOfFirstInstructionOrThrow
import app.revanced.util.indexOfFirstInstructionReversedOrThrow
import app.revanced.util.returnEarly import app.revanced.util.returnEarly
import app.revanced.util.valueOrThrow import app.revanced.util.valueOrThrow
import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction21c 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.FiveRegisterInstruction
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
import com.android.tools.smali.dexlib2.iface.instruction.formats.Instruction21c 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.MethodReference
import com.android.tools.smali.dexlib2.iface.reference.StringReference import com.android.tools.smali.dexlib2.iface.reference.StringReference
@ -233,41 +230,18 @@ fun gmsCoreSupportPatch(
} }
} }
fun transformPrimeMethod() { fun transformPrimeMethod(packageName: String) {
setOf( primeMethodFingerprint.methodOrThrow().apply {
primesBackgroundInitializationFingerprint, var register = 2
primesLifecycleEventFingerprint
).forEach { fingerprint -> val index = instructions.indexOfFirst {
fingerprint.methodOrThrow().apply { if (it.getReference<StringReference>()?.string != fromPackageName) return@indexOfFirst false
val exceptionIndex = indexOfFirstInstructionReversedOrThrow {
opcode == Opcode.NEW_INSTANCE && register = (it as OneRegisterInstruction).registerA
(this as? ReferenceInstruction)?.reference?.toString() == "Ljava/lang/IllegalStateException;" return@indexOfFirst true
}
val index =
indexOfFirstInstructionReversedOrThrow(exceptionIndex, Opcode.IF_EQZ)
val register = getInstruction<OneRegisterInstruction>(index).registerA
addInstruction(
index,
"const/4 v$register, 0x1"
)
}
}
primesApiFingerprint.mutableClassOrThrow().methods.filter { method ->
method.name != "<clinit>" &&
method.returnType == "V"
}.forEach { method ->
method.apply {
val index = if (MethodUtil.isConstructor(method))
indexOfFirstInstructionOrThrow {
opcode == Opcode.INVOKE_DIRECT &&
getReference<MethodReference>()?.name == "<init>"
} + 1
else 0
addInstruction(
index,
"return-void"
)
} }
replaceInstruction(index, "const-string v$register, \"$packageName\"")
} }
} }
@ -293,14 +267,12 @@ fun gmsCoreSupportPatch(
// Return these methods early to prevent the app from crashing. // Return these methods early to prevent the app from crashing.
setOf( setOf(
castContextFetchFingerprint, castContextFetchFingerprint,
castDynamiteModuleFingerprint,
castDynamiteModuleV2Fingerprint,
googlePlayUtilityFingerprint, googlePlayUtilityFingerprint,
serviceCheckFingerprint, serviceCheckFingerprint,
).forEach { it.methodOrThrow().returnEarly() } ).forEach { it.methodOrThrow().returnEarly() }
// Specific method that needs to be patched. // Specific method that needs to be patched.
transformPrimeMethod() transformPrimeMethod(packageName)
// Verify GmsCore is installed and whitelisted for power optimizations and background usage. // Verify GmsCore is installed and whitelisted for power optimizations and background usage.
mainActivityOnCreateFingerprint.method.apply { mainActivityOnCreateFingerprint.method.apply {
@ -358,134 +330,262 @@ private object Constants {
* All permissions. * All permissions.
*/ */
val PERMISSIONS = setOf( val PERMISSIONS = setOf(
// C2DM / GCM
"com.google.android.c2dm.permission.RECEIVE", "com.google.android.c2dm.permission.RECEIVE",
"com.google.android.c2dm.permission.SEND", "com.google.android.c2dm.permission.SEND",
"com.google.android.gtalkservice.permission.GTALK_SERVICE", "com.google.android.gms.auth.api.phone.permission.SEND",
"com.google.android.providers.gsf.permission.READ_GSERVICES", "com.google.android.gms.permission.AD_ID",
"com.google.android.gms.permission.AD_ID_NOTIFICATION",
// GAuth "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",
"com.google.android.googleapps.permission.GOOGLE_AUTH.cp", "com.google.android.googleapps.permission.GOOGLE_AUTH.cp",
"com.google.android.googleapps.permission.GOOGLE_AUTH.local", "com.google.android.googleapps.permission.GOOGLE_AUTH.local",
"com.google.android.googleapps.permission.GOOGLE_AUTH.mail", "com.google.android.googleapps.permission.GOOGLE_AUTH.mail",
"com.google.android.googleapps.permission.GOOGLE_AUTH.writely", "com.google.android.googleapps.permission.GOOGLE_AUTH.writely",
"com.google.android.gtalkservice.permission.GTALK_SERVICE",
// Ad "com.google.android.providers.gsf.permission.READ_GSERVICES",
"com.google.android.gms.permission.AD_ID_NOTIFICATION",
"com.google.android.gms.permission.AD_ID",
) )
/** /**
* All intent actions. * All intent actions.
*/ */
val ACTIONS = setOf( val ACTIONS = setOf(
// location "com.google.android.c2dm.intent.RECEIVE",
"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.REGISTER", "com.google.android.c2dm.intent.REGISTER",
"com.google.android.c2dm.intent.REGISTRATION", "com.google.android.c2dm.intent.REGISTRATION",
"com.google.android.c2dm.intent.UNREGISTER", "com.google.android.c2dm.intent.UNREGISTER",
"com.google.android.c2dm.intent.RECEIVE", "com.google.android.contextmanager.service.ContextManagerService.START",
"com.google.iid.TOKEN_REQUEST",
"com.google.android.gcm.intent.SEND", "com.google.android.gcm.intent.SEND",
"com.google.android.gms.accounts.ACCOUNT_SERVICE",
// car "com.google.android.gms.accountsettings.ACCOUNT_PREFERENCES_SETTINGS",
"com.google.android.gms.car.service.START", "com.google.android.gms.accountsettings.action.BROWSE_SETTINGS",
"com.google.android.gms.accountsettings.action.VIEW_SETTINGS",
// people "com.google.android.gms.accountsettings.MY_ACCOUNT",
"com.google.android.gms.people.service.START", "com.google.android.gms.accountsettings.PRIVACY_SETTINGS",
"com.google.android.gms.accountsettings.SECURITY_SETTINGS",
// wearable "com.google.android.gms.ads.gservice.START",
"com.google.android.gms.wearable.BIND", "com.google.android.gms.ads.identifier.service.EVENT_ATTESTATION",
"com.google.android.gms.ads.service.CACHE",
// auth "com.google.android.gms.ads.service.CONSENT_LOOKUP",
"com.google.android.gsf.login", "com.google.android.gms.ads.service.HTTP",
"com.google.android.gsf.action.GET_GLS", "com.google.android.gms.analytics.service.START",
"com.google.android.gms.common.account.CHOOSE_ACCOUNT", "com.google.android.gms.app.settings.GoogleSettingsLink",
"com.google.android.gms.auth.login.LOGIN", "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.PICKER",
"com.google.android.gms.auth.api.credentials.service.START", "com.google.android.gms.auth.api.credentials.service.START",
"com.google.android.gms.auth.service.START", "com.google.android.gms.auth.api.identity.service.authorization.START",
"com.google.firebase.auth.api.gms.service.START", "com.google.android.gms.auth.api.identity.service.credentialsaving.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.signin.START", "com.google.android.gms.auth.api.identity.service.signin.START",
"com.google.android.gms.accountsettings.action.VIEW_SETTINGS", "com.google.android.gms.auth.api.phone.service.InternalService.START",
"com.google.android.gms.auth.api.signin.service.START",
// fido "com.google.android.gms.auth.be.appcert.AppCertService",
"com.google.android.gms.fido.fido2.privileged.START", "com.google.android.gms.auth.blockstore.service.START",
"com.google.android.gms.auth.config.service.START",
// gass "com.google.android.gms.auth.cryptauth.cryptauthservice.START",
"com.google.android.gms.gass.START", "com.google.android.gms.auth.GOOGLE_SIGN_IN",
"com.google.android.gms.auth.login.LOGIN",
// games "com.google.android.gms.auth.proximity.devicesyncservice.START",
"com.google.android.gms.games.service.START", "com.google.android.gms.auth.proximity.securechannelservice.START",
"com.google.android.gms.games.PLAY_GAMES_UPGRADE", "com.google.android.gms.auth.proximity.START",
"com.google.android.gms.games.internal.connect.service.START", "com.google.android.gms.auth.service.START",
"com.google.android.gms.backup.ACTION_BACKUP_SETTINGS",
// help "com.google.android.gms.backup.G1_BACKUP",
"com.google.android.gms.googlehelp.service.GoogleHelpService.START", "com.google.android.gms.backup.G1_RESTORE",
"com.google.android.gms.googlehelp.HELP", "com.google.android.gms.backup.GMS_MODULE_RESTORE",
"com.google.android.gms.feedback.internal.IFeedbackService", "com.google.android.gms.beacon.internal.IBleService.START",
"com.google.android.gms.car.service.START",
// cast "com.google.android.gms.carrierauth.service.START",
"com.google.android.gms.cast.firstparty.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", "com.google.android.gms.cast.service.BIND_CAST_DEVICE_CONTROLLER_SERVICE",
"com.google.android.gms.cast_mirroring.service.START",
// fonts "com.google.android.gms.checkin.BIND_TO_SERVICE",
"com.google.android.gms.fonts", "com.google.android.gms.chromesync.service.START",
// 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.clearcut.service.START", "com.google.android.gms.clearcut.service.START",
"com.google.android.gms.icing.LIGHTWEIGHT_INDEX_SERVICE", "com.google.android.gms.common.account.CHOOSE_ACCOUNT",
"com.google.android.gms.icing.INDEX_SERVICE", "com.google.android.gms.common.download.START",
"com.google.android.gms.mdm.services.START", "com.google.android.gms.common.service.START",
"com.google.android.gms.common.telemetry.service.START",
// potoken "com.google.android.gms.config.START",
"com.google.android.gms.potokens.service.START", "com.google.android.gms.constellation.service.START",
"com.google.android.gms.credential.manager.service.firstparty.START",
// droidguard, safetynet "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.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.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. * All content provider authorities.
*/ */
val AUTHORITIES = setOf( 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.android.gsf.gservices",
"com.google.settings", "com.google.settings",
// auth
"com.google.android.gms.auth.accounts",
// fonts
"com.google.android.gms.fonts",
// phenotype
"com.google.android.gms.phenotype",
) )
} }

View File

@ -18,6 +18,10 @@ lateinit var onConfigurationChangedMethod: MutableMethod
private set private set
lateinit var onCreateMethod: MutableMethod lateinit var onCreateMethod: MutableMethod
private set private set
lateinit var onStartMethod: MutableMethod
private set
lateinit var onStopMethod: MutableMethod
private set
private lateinit var constructorMethod: MutableMethod private lateinit var constructorMethod: MutableMethod
private lateinit var onBackPressedMethod: MutableMethod private lateinit var onBackPressedMethod: MutableMethod
@ -45,6 +49,9 @@ fun baseMainActivityResolvePatch(
// set onConfigurationChanged method // set onConfigurationChanged method
onConfigurationChangedMethod = getMainActivityMethod("onConfigurationChanged") onConfigurationChangedMethod = getMainActivityMethod("onConfigurationChanged")
onStartMethod = getMainActivityMethod("onStart")
onStopMethod = getMainActivityMethod("onStop")
} }
} }

View File

@ -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"
)
}

View File

@ -6,13 +6,11 @@ import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWith
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
import app.revanced.patcher.extensions.InstructionExtensions.instructions import app.revanced.patcher.extensions.InstructionExtensions.instructions
import app.revanced.patcher.extensions.InstructionExtensions.removeInstruction 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.BytecodePatchBuilder
import app.revanced.patcher.patch.BytecodePatchContext import app.revanced.patcher.patch.BytecodePatchContext
import app.revanced.patcher.patch.PatchException import app.revanced.patcher.patch.PatchException
import app.revanced.patcher.patch.bytecodePatch import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable 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.extension.Constants.SPOOF_PATH
import app.revanced.patches.shared.formatStreamModelConstructorFingerprint import app.revanced.patches.shared.formatStreamModelConstructorFingerprint
import app.revanced.util.findInstructionIndicesReversedOrThrow 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.injectLiteralInstructionBooleanCall
import app.revanced.util.fingerprint.matchOrThrow import app.revanced.util.fingerprint.matchOrThrow
import app.revanced.util.fingerprint.methodOrThrow import app.revanced.util.fingerprint.methodOrThrow
import app.revanced.util.fingerprint.mutableClassOrThrow
import app.revanced.util.getReference import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstructionOrThrow import app.revanced.util.indexOfFirstInstructionOrThrow
import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.AccessFlags
@ -122,10 +121,15 @@ fun baseSpoofStreamingDataPatch(
} }
} }
val streamingDataFormatsReference = with(videoStreamingDataConstructorFingerprint.methodOrThrow(videoStreamingDataToStringFingerprint)) { val streamingDataFormatsReference = with(
videoStreamingDataConstructorFingerprint.methodOrThrow(
videoStreamingDataToStringFingerprint
)
) {
val getFormatsFieldIndex = indexOfGetFormatsFieldInstruction(this) val getFormatsFieldIndex = indexOfGetFormatsFieldInstruction(this)
val longMaxValueIndex = indexOfLongMaxValueInstruction(this, getFormatsFieldIndex) val longMaxValueIndex = indexOfLongMaxValueInstruction(this, getFormatsFieldIndex)
val longMaxValueRegister = getInstruction<OneRegisterInstruction>(longMaxValueIndex).registerA val longMaxValueRegister =
getInstruction<OneRegisterInstruction>(longMaxValueIndex).registerA
val videoIdIndex = val videoIdIndex =
indexOfFirstInstructionOrThrow(longMaxValueIndex) { indexOfFirstInstructionOrThrow(longMaxValueIndex) {
val reference = getReference<FieldReference>() val reference = getReference<FieldReference>()
@ -362,12 +366,23 @@ fun baseSpoofStreamingDataPatch(
// endregion // endregion
findMethodOrThrow("$PATCHES_PATH/PatchStatus;") { // region Set DroidGuard poToken.
name == "SpoofStreamingData"
}.replaceInstruction( poTokenToStringFingerprint.mutableClassOrThrow().let {
0, val poTokenClass = it.fields.find { field ->
"const/4 v0, 0x1" field.accessFlags == AccessFlags.PRIVATE.value && field.type.startsWith("L")
}!!.type
findMethodOrThrow(poTokenClass) {
name == "<init>" &&
parameters == listOf("[B")
}.addInstruction(
1,
"invoke-static { p1 }, $EXTENSION_CLASS_DESCRIPTOR->setDroidGuardPoToken([B)V"
) )
}
// endregion
executeBlock() executeBlock()

View File

@ -197,3 +197,22 @@ internal val hlsCurrentTimeFingerprint = legacyFingerprint(
parameters = listOf("Z", "L"), parameters = listOf("Z", "L"),
literals = listOf(HLS_CURRENT_TIME_FEATURE_FLAG), 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
},
)

View File

@ -134,3 +134,12 @@ internal val tooltipContentViewFingerprint = legacyFingerprint(
literals = listOf(toolTipContentView), 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)
)

View File

@ -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_CLASS_DESCRIPTOR
import app.revanced.patches.youtube.utils.extension.Constants.GENERAL_PATH 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.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.accountSwitcherAccessibility
import app.revanced.patches.youtube.utils.resourceid.sharedResourceIdPatch import app.revanced.patches.youtube.utils.resourceid.sharedResourceIdPatch
import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference
import app.revanced.patches.youtube.utils.settings.settingsPatch 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.matchOrThrow
import app.revanced.util.fingerprint.methodOrThrow import app.revanced.util.fingerprint.methodOrThrow
import app.revanced.util.fingerprint.mutableClassOrThrow import app.revanced.util.fingerprint.mutableClassOrThrow
@ -55,10 +58,16 @@ val layoutComponentsPatch = bytecodePatch(
sharedResourceIdPatch, sharedResourceIdPatch,
settingsMenuPatch, settingsMenuPatch,
viewGroupMarginLayoutParamsHookPatch, viewGroupMarginLayoutParamsHookPatch,
versionCheckPatch,
) )
execute { execute {
var settingArray = arrayOf(
"PREFERENCE_SCREEN: GENERAL",
"SETTINGS: HIDE_LAYOUT_COMPONENTS"
)
// region patch for disable pip notification // region patch for disable pip notification
pipNotificationFingerprint.matchOrThrow().let { pipNotificationFingerprint.matchOrThrow().let {
@ -86,6 +95,19 @@ val layoutComponentsPatch = bytecodePatch(
// endregion // 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 // region patch for disable update screen
appBlockingCheckResultToStringFingerprint.mutableClassOrThrow().methods.first { method -> appBlockingCheckResultToStringFingerprint.mutableClassOrThrow().methods.first { method ->
@ -234,10 +256,7 @@ val layoutComponentsPatch = bytecodePatch(
// region add settings // region add settings
addPreference( addPreference(
arrayOf( settingArray,
"PREFERENCE_SCREEN: GENERAL",
"SETTINGS: HIDE_LAYOUT_COMPONENTS"
),
HIDE_LAYOUT_COMPONENTS HIDE_LAYOUT_COMPONENTS
) )

View File

@ -74,32 +74,9 @@ internal val setEnumMapFingerprint = legacyFingerprint(
literals = listOf(ytFillBell), 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( internal val translucentNavigationBarFingerprint = legacyFingerprint(
name = "translucentNavigationStatusBarFeatureFlagFingerprint", name = "translucentNavigationBarFingerprint",
accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, literals = listOf(TRANSLUCENT_NAVIGATION_BAR_FEATURE_FLAG),
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)
) )

View File

@ -85,30 +85,11 @@ val navigationBarComponentsPatch = bytecodePatch(
// region patch for enable translucent navigation bar // region patch for enable translucent navigation bar
if (is_19_25_or_greater) { if (is_19_25_or_greater) {
arrayOf( translucentNavigationBarFingerprint.injectLiteralInstructionBooleanCall(
Triple( TRANSLUCENT_NAVIGATION_BAR_FEATURE_FLAG,
translucentNavigationStatusBarFeatureFlagFingerprint, "$GENERAL_CLASS_DESCRIPTOR->enableTranslucentNavigationBar()Z"
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"
)
}
settingArray += "SETTINGS: DISABLE_TRANSLUCENT_STATUS_BAR"
settingArray += "SETTINGS: TRANSLUCENT_NAVIGATION_BAR" settingArray += "SETTINGS: TRANSLUCENT_NAVIGATION_BAR"
} }

View File

@ -342,7 +342,8 @@ val toolBarComponentsPatch = bytecodePatch(
opcode == Opcode.INVOKE_VIRTUAL && opcode == Opcode.INVOKE_VIRTUAL &&
getReference<MethodReference>()?.toString() == voiceInputControllerActivityMethodCall getReference<MethodReference>()?.toString() == voiceInputControllerActivityMethodCall
} }
val setOnClickListenerIndex = indexOfFirstInstructionOrThrow(voiceInputControllerActivityIndex) { val setOnClickListenerIndex =
indexOfFirstInstructionOrThrow(voiceInputControllerActivityIndex) {
opcode == Opcode.INVOKE_VIRTUAL && opcode == Opcode.INVOKE_VIRTUAL &&
getReference<MethodReference>()?.name == "setOnClickListener" getReference<MethodReference>()?.name == "setOnClickListener"
} }

View File

@ -5,12 +5,14 @@ import app.revanced.patcher.patch.resourcePatch
import app.revanced.patcher.patch.stringOption import app.revanced.patcher.patch.stringOption
import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE
import app.revanced.patches.youtube.utils.patch.PatchList.CUSTOM_BRANDING_ICON_FOR_YOUTUBE 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_32_or_greater
import app.revanced.patches.youtube.utils.playservice.is_19_34_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.playservice.versionCheckPatch
import app.revanced.patches.youtube.utils.settings.ResourceUtils.updatePatchStatusIcon import app.revanced.patches.youtube.utils.settings.ResourceUtils.updatePatchStatusIcon
import app.revanced.patches.youtube.utils.settings.settingsPatch import app.revanced.patches.youtube.utils.settings.settingsPatch
import app.revanced.util.ResourceGroup import app.revanced.util.ResourceGroup
import app.revanced.util.Utils.printWarn
import app.revanced.util.Utils.trimIndentMultiline import app.revanced.util.Utils.trimIndentMultiline
import app.revanced.util.copyAdaptiveIcon import app.revanced.util.copyAdaptiveIcon
import app.revanced.util.copyFile import app.revanced.util.copyFile
@ -122,7 +124,7 @@ val customBrandingIconPatch = resourcePatch(
key = "changeSplashIcon", key = "changeSplashIcon",
default = true, default = true,
title = "Change splash icons", 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 required = true
) )
@ -171,6 +173,7 @@ val customBrandingIconPatch = resourcePatch(
// Change splash icon. // Change splash icon.
if (changeSplashIconOption == true) { if (changeSplashIconOption == true) {
if (!is_19_17_or_greater) {
splashIconResourceGroups.let { resourceGroups -> splashIconResourceGroups.let { resourceGroups ->
resourceGroups.forEach { resourceGroups.forEach {
copyResources("$appIconResourcePath/splash", it) copyResources("$appIconResourcePath/splash", it)
@ -195,6 +198,9 @@ val customBrandingIconPatch = resourcePatch(
} }
} }
} }
} else {
printWarn("\"Change splash icons\" is not supported in this version. Use YouTube 19.16.39 or earlier.")
}
} }
// Change splash screen. // Change splash screen.

View File

@ -1,14 +1,13 @@
package app.revanced.patches.youtube.layout.theme package app.revanced.patches.youtube.layout.theme
import app.revanced.patcher.patch.resourcePatch 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.compatibility.Constants.COMPATIBLE_PACKAGE
import app.revanced.patches.youtube.utils.patch.PatchList.MATERIALYOU 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.addPreference
import app.revanced.patches.youtube.utils.settings.ResourceUtils.updatePatchStatusTheme import app.revanced.patches.youtube.utils.settings.ResourceUtils.updatePatchStatusTheme
import app.revanced.patches.youtube.utils.settings.settingsPatch import app.revanced.patches.youtube.utils.settings.settingsPatch
import app.revanced.util.copyXmlNode import app.revanced.util.copyXmlNode
import org.w3c.dom.Element
import java.nio.file.Files
@Suppress("unused") @Suppress("unused")
val materialYouPatch = resourcePatch( val materialYouPatch = resourcePatch(
@ -24,117 +23,7 @@ val materialYouPatch = resourcePatch(
) )
execute { execute {
fun patchXmlFile( baseMaterialYou()
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"
)
copyXmlNode("youtube/materialyou/host", "values-v31/colors.xml", "resources") copyXmlNode("youtube/materialyou/host", "values-v31/colors.xml", "resources")

Some files were not shown because too many files have changed in this diff Show More