mirror of
https://github.com/inotia00/revanced-patches.git
synced 2025-05-08 10:34:33 +02:00
Merge branch 'dev' into revanced-extended
This commit is contained in:
commit
1aaf03df50
11
README.md
11
README.md
@ -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 |
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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() {
|
||||||
|
@ -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
|
||||||
};
|
};
|
||||||
|
|
||||||
public static int getLithoColor(int originalValue) {
|
// background colors
|
||||||
if (anyEquals(originalValue, DARK_VALUES))
|
private static final Drawable headerGradient =
|
||||||
return -16777215;
|
ResourceUtils.getDrawable("revanced_header_gradient");
|
||||||
|
private static final int blackColor =
|
||||||
|
ResourceUtils.getColor("yt_black1");
|
||||||
|
private static final int elementsContainerIdentifier =
|
||||||
|
ResourceUtils.getIdIdentifier("elements_container");
|
||||||
|
|
||||||
return originalValue;
|
public static int getLithoColor(int originalValue) {
|
||||||
|
return ArrayUtils.contains(DARK_VALUES, originalValue)
|
||||||
|
? blackColor
|
||||||
|
: 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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) {
|
||||||
|
@ -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) {
|
||||||
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 What’s 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 What’s 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 What’s 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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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 `What’s 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 `What’s 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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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 + '\'' + '}';
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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.
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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) {
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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.
|
||||||
*
|
*
|
||||||
|
@ -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
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -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),
|
||||||
|
@ -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) {
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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;
|
||||||
|
@ -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();
|
||||||
|
@ -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) {
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
@ -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());
|
||||||
|
@ -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
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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.
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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"));
|
||||||
|
@ -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) {
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
@ -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
3
gradlew
vendored
@ -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
|
||||||
|
114
patches.json
114
patches.json
@ -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",
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
|
||||||
)
|
|
@ -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)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
)
|
@ -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)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -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.")
|
||||||
|
)
|
@ -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"
|
||||||
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
@ -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",
|
||||||
|
@ -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."
|
||||||
|
@ -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
|
||||||
|
@ -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",
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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
|
||||||
|
)
|
||||||
|
)
|
@ -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)
|
||||||
|
}
|
@ -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))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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",
|
||||||
|
@ -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(
|
||||||
|
@ -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",
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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"
|
||||||
|
)
|
||||||
|
}
|
@ -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
|
||||||
@ -117,15 +116,20 @@ fun baseSpoofStreamingDataPatch(
|
|||||||
// region Replace the streaming data.
|
// region Replace the streaming data.
|
||||||
|
|
||||||
val approxDurationMsReference = formatStreamModelConstructorFingerprint.matchOrThrow().let {
|
val approxDurationMsReference = formatStreamModelConstructorFingerprint.matchOrThrow().let {
|
||||||
with (it.method) {
|
with(it.method) {
|
||||||
getInstruction<ReferenceInstruction>(it.patternMatch!!.startIndex).reference
|
getInstruction<ReferenceInstruction>(it.patternMatch!!.startIndex).reference
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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()
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
},
|
||||||
|
)
|
||||||
|
@ -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)
|
||||||
|
)
|
||||||
|
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -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)
|
|
||||||
)
|
)
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
|
@ -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.
|
||||||
|
@ -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
Loading…
x
Reference in New Issue
Block a user