Merge remote-tracking branch 'origin/dev' into feat/update-youtube-patches

# Conflicts:
#	extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/YouTubeAndMusicSettings.java
This commit is contained in:
oSumAtrIX 2026-03-19 20:44:17 +01:00
commit cc019ec78b
No known key found for this signature in database
GPG key ID: A9B3094ACDB604B4
173 changed files with 30882 additions and 17056 deletions

View file

@ -1,14 +1,7 @@
android {
namespace = "app.revanced.extension"
defaultConfig {
minSdk = 21
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
}
dependencies {

View file

@ -1,9 +1,6 @@
android {
namespace = "app.revanced.extension"
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
defaultConfig {
minSdk = 23
}
}

View file

@ -1,14 +1,7 @@
android {
namespace = "app.revanced.extension"
defaultConfig {
minSdk = 21
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
}
dependencies {

View file

@ -31,7 +31,10 @@ public class InternalDataDocumentsProvider extends DocumentsProvider {
private static final String[] directoryColumns =
{"document_id", "mime_type", "_display_name", "last_modified", "flags",
"_size", "full_path", "lstat_info"};
private static final int S_IFLNK = 0x8000;
@SuppressWarnings("OctalInteger")
private static final int S_IFMT = 0170000;
@SuppressWarnings("OctalInteger")
private static final int S_IFLNK = 0120000;
private String packageName;
private File dataDirectory;
@ -47,7 +50,7 @@ public class InternalDataDocumentsProvider extends DocumentsProvider {
if (root.isDirectory()) {
try {
// Only delete recursively if the directory is not a symlink
if ((Os.lstat(root.getPath()).st_mode & S_IFLNK) != S_IFLNK) {
if ((Os.lstat(root.getPath()).st_mode & S_IFMT) != S_IFLNK) {
File[] files = root.listFiles();
if (files != null) {
for (File file : files) {
@ -324,7 +327,7 @@ public class InternalDataDocumentsProvider extends DocumentsProvider {
sb.append(";");
sb.append(lstat.st_gid);
// Append symlink target if it is a symlink
if ((lstat.st_mode & S_IFLNK) == S_IFLNK) {
if ((lstat.st_mode & S_IFMT) == S_IFLNK) {
sb.append(";");
sb.append(Os.readlink(path));
}

View file

@ -1,15 +1,8 @@
android {
namespace = "app.revanced.extension"
defaultConfig {
minSdk = 21
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
buildFeatures {
aidl = true
}

View file

@ -1,4 +1,4 @@
package app.revanced.extension.playintegrity;
package app.revanced.extension.play;
import android.content.Context;
import android.content.Intent;

View file

@ -1,14 +1,7 @@
android {
namespace = "app.revanced.extension"
defaultConfig {
minSdk = 21
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
}
dependencies {

View file

@ -1,14 +1,7 @@
android {
namespace = "app.revanced.extension"
defaultConfig {
minSdk = 21
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
}
dependencies {

View file

@ -3,3 +3,9 @@ dependencies {
compileOnly(libs.annotation)
compileOnly(libs.okhttp)
}
android {
defaultConfig {
minSdk = 22
}
}

View file

@ -4,3 +4,9 @@ dependencies {
compileOnly(libs.annotation)
compileOnly(libs.okhttp)
}
android {
defaultConfig {
minSdk = 21
}
}

View file

@ -1,4 +1,10 @@
dependencies {
compileOnly(project(":extensions:shared:library"))
compileOnly(project(":extensions:cricbuzz:stub"))
}
}
android {
defaultConfig {
minSdk = 21
}
}

View file

@ -1,3 +1,9 @@
dependencies {
compileOnly(project(":extensions:shared:library"))
}
android {
defaultConfig {
minSdk = 26
}
}

View file

@ -1,3 +1,9 @@
dependencies {
compileOnly(project(":extensions:shared:library"))
}
android {
defaultConfig {
minSdk = 24
}
}

View file

@ -1,7 +1,7 @@
package app.revanced.extension.music.patches.spoof;
import static app.revanced.extension.music.settings.Settings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE;
import static app.revanced.extension.shared.spoof.ClientType.ANDROID_NO_SDK;
import static app.revanced.extension.shared.spoof.ClientType.ANDROID_REEL;
import static app.revanced.extension.shared.spoof.ClientType.ANDROID_VR_1_43_32;
import static app.revanced.extension.shared.spoof.ClientType.ANDROID_VR_1_61_48;
import static app.revanced.extension.shared.spoof.ClientType.VISIONOS;
@ -18,8 +18,8 @@ public class SpoofVideoStreamsPatch {
*/
public static void setClientOrderToUse() {
List<ClientType> availableClients = List.of(
ANDROID_REEL,
ANDROID_VR_1_43_32,
ANDROID_NO_SDK,
VISIONOS,
ANDROID_VR_1_61_48
);

View file

@ -40,7 +40,7 @@ public class Settings extends YouTubeAndMusicSettings {
// Miscellaneous
public static final EnumSetting<ClientType> SPOOF_VIDEO_STREAMS_CLIENT_TYPE = new EnumSetting<>("revanced_spoof_video_streams_client_type",
ClientType.ANDROID_VR_1_43_32, true, parent(SPOOF_VIDEO_STREAMS));
ClientType.ANDROID_REEL, true, parent(SPOOF_VIDEO_STREAMS));
public static final BooleanSetting FORCE_ORIGINAL_AUDIO = new BooleanSetting("revanced_force_original_audio", TRUE, true);
}

View file

@ -5,11 +5,6 @@ dependencies {
android {
defaultConfig {
minSdk = 26
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
minSdk = 23
}
}

View file

@ -2,3 +2,9 @@ dependencies {
compileOnly(project(":extensions:shared:library"))
compileOnly(project(":extensions:nunl:stub"))
}
android {
defaultConfig {
minSdk = 26
}
}

View file

@ -2,3 +2,9 @@ dependencies {
compileOnly(project(":extensions:shared:library"))
compileOnly(project(":extensions:primevideo:stub"))
}
android {
defaultConfig {
minSdk = 21
}
}

View file

@ -1,3 +1,9 @@
dependencies {
compileOnly(project(":extensions:reddit:stub"))
}
android {
defaultConfig {
minSdk = 28
}
}

View file

@ -2,3 +2,9 @@ dependencies {
compileOnly(project(":extensions:shared:library"))
compileOnly(project(":extensions:samsung:radio:stub"))
}
android {
defaultConfig {
minSdk = 26
}
}

View file

@ -5,6 +5,6 @@ dependencies {
android {
defaultConfig {
minSdk = 26
minSdk = 23
}
}

View file

@ -1,5 +1,5 @@
plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.android.library)
}
android {
@ -19,4 +19,6 @@ android {
dependencies {
compileOnly(libs.annotation)
compileOnly(libs.okhttp)
compileOnly(libs.protobuf.javalite)
implementation(project(":extensions:shared:protobuf", configuration = "shadowRuntimeElements"))
}

View file

@ -32,11 +32,7 @@ import android.view.Window;
import android.view.WindowManager;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.Toast;
import android.widget.Toolbar;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
@ -65,7 +61,6 @@ import app.revanced.extension.shared.settings.BooleanSetting;
import app.revanced.extension.shared.settings.preference.ReVancedAboutPreference;
import app.revanced.extension.shared.ui.Dim;
@SuppressWarnings("NewApi")
public class Utils {
@SuppressLint("StaticFieldLeak")
@ -139,6 +134,7 @@ public class Utils {
return versionName;
}
@SuppressWarnings("unused")
public static String getApplicationName() {
if (applicationLabel == null) {
try {
@ -185,24 +181,13 @@ public class Utils {
* @param view The view to hide.
*/
public static void hideViewBy0dp(View view) {
if (view instanceof LinearLayout) {
LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(0, 0);
view.setLayoutParams(layoutParams);
} else if (view instanceof FrameLayout) {
FrameLayout.LayoutParams layoutParams2 = new FrameLayout.LayoutParams(0, 0);
view.setLayoutParams(layoutParams2);
} else if (view instanceof RelativeLayout) {
RelativeLayout.LayoutParams layoutParams3 = new RelativeLayout.LayoutParams(0, 0);
view.setLayoutParams(layoutParams3);
} else if (view instanceof Toolbar) {
Toolbar.LayoutParams layoutParams4 = new Toolbar.LayoutParams(0, 0);
view.setLayoutParams(layoutParams4);
} else {
ViewGroup.LayoutParams params = view.getLayoutParams();
params.width = 0;
params.height = 0;
view.setLayoutParams(params);
}
ViewGroup.LayoutParams params = view.getLayoutParams();
if (params == null)
params = new ViewGroup.LayoutParams(0, 0);
params.width = 0;
params.height = 0;
view.setLayoutParams(params);
}
/**
@ -514,6 +499,7 @@ public class Utils {
return str != null && !str.isEmpty();
}
@SuppressWarnings("unused")
public static boolean isTablet() {
return context.getResources().getConfiguration().smallestScreenWidthDp >= 600;
}
@ -553,6 +539,7 @@ public class Utils {
return getTextDirectionString(isRightToLeftLocale());
}
@SuppressWarnings("unused")
public static String getTextDirectionString(Locale locale) {
return getTextDirectionString(isRightToLeftLocale(locale));
}

View file

@ -4,12 +4,12 @@ import static android.text.Html.FROM_HTML_MODE_COMPACT;
import static app.revanced.extension.shared.StringRef.str;
import static app.revanced.extension.shared.Utils.DialogFragmentOnStartAction;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.Dialog;
import android.content.Intent;
import android.graphics.PorterDuff;
import android.net.Uri;
import android.os.Build;
import android.text.Html;
import android.util.Pair;
import android.view.Gravity;
@ -19,6 +19,7 @@ import android.widget.ImageView;
import android.widget.LinearLayout;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import java.util.Collection;
@ -28,6 +29,7 @@ import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.ui.CustomDialog;
@RequiresApi(api = Build.VERSION_CODES.N)
abstract class Check {
private static final int NUMBER_OF_TIMES_TO_IGNORE_WARNING_BEFORE_DISABLING = 2;
@ -76,7 +78,6 @@ abstract class Check {
BaseSettings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.save(Integer.MAX_VALUE);
}
@SuppressLint("NewApi")
static void issueWarning(Activity activity, Collection<Check> failedChecks) {
final var reasons = new StringBuilder();

View file

@ -10,6 +10,8 @@ import android.util.Base64;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
@ -29,6 +31,7 @@ import static app.revanced.extension.shared.checks.PatchInfo.Build.*;
* <br>
* Various indicators help to detect if the app was patched by the user.
*/
@RequiresApi(api = Build.VERSION_CODES.N)
@SuppressWarnings("unused")
public final class CheckEnvironmentPatch {
private static final boolean DEBUG_ALWAYS_SHOW_CHECK_FAILED_DIALOG = debugAlwaysShowWarning();
@ -121,7 +124,7 @@ public final class CheckEnvironmentPatch {
* If the build properties are different, the app was likely downloaded pre-patched or patched on another device.
*/
private static class CheckWasPatchedOnSameDevice extends Check {
@SuppressLint({"NewApi", "HardwareIds"})
@SuppressLint("HardwareIds")
@Override
protected Boolean check() {
if (PATCH_BOARD.isEmpty()) {
@ -195,7 +198,7 @@ public final class CheckEnvironmentPatch {
PackageManager packageManager = context.getPackageManager();
PackageInfo packageInfo = packageManager.getPackageInfo(context.getPackageName(), 0);
// Duration since initial install or last update, which ever is sooner.
// Duration since initial install or last update, whichever is sooner.
durationBetweenPatchingAndInstallation = packageInfo.lastUpdateTime - PatchInfo.PATCH_TIME;
Logger.printInfo(() -> "App was installed/updated: "
+ (durationBetweenPatchingAndInstallation / (60 * 1000) + " minutes after patching"));

View file

@ -114,7 +114,7 @@ public class CustomBrandingPatch {
/**
* Injection point.
*
* <p>
* The total number of app name aliases, including dummy aliases.
*/
private static int numberOfPresetAppNames() {
@ -146,13 +146,13 @@ public class CustomBrandingPatch {
public static int getDefaultAppNameIndex() {
return userProvidedCustomName()
? numberOfPresetAppNames()
: 1;
: 2;
}
public static BrandingTheme getDefaultIconStyle() {
return userProvidedCustomIcon()
? BrandingTheme.CUSTOM
: BrandingTheme.ORIGINAL;
: BrandingTheme.ROUNDED;
}
/**

View file

@ -47,7 +47,7 @@ public final class LithoFilterPatch {
}
builder.append(" Path: ");
builder.append(path);
if (YouTubeAndMusicSettings.DEBUG_PROTOBUFFER.get()) {
if (YouTubeAndMusicSettings.DEBUG_PROTOCOLBUFFER.get()) {
builder.append(" BufferStrings: ");
findAsciiStrings(builder, buffer);
}

View file

@ -6,12 +6,15 @@ import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.preference.PreferenceFragment;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import android.widget.Toolbar;
import androidx.annotation.RequiresApi;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.ResourceType;
import app.revanced.extension.shared.Utils;
@ -22,7 +25,8 @@ import app.revanced.extension.shared.ui.Dim;
* Base class for hooking activities to inject a custom PreferenceFragment with a toolbar.
* Provides common logic for initializing the activity and setting up the toolbar.
*/
@SuppressWarnings({"deprecation", "NewApi"})
@SuppressWarnings("deprecation")
@RequiresApi(api = Build.VERSION_CODES.O)
public abstract class BaseActivityHook extends Activity {
private static final int ID_REVANCED_SETTINGS_FRAGMENTS =

View file

@ -47,7 +47,7 @@ public class BaseSettings {
//
public static final BooleanSetting SPOOF_VIDEO_STREAMS = new BooleanSetting("revanced_spoof_video_streams", TRUE, true, "revanced_spoof_video_streams_user_dialog_message");
public static final BooleanSetting SPOOF_STREAMING_DATA_STATS_FOR_NERDS = new BooleanSetting("revanced_spoof_streaming_data_stats_for_nerds", TRUE, parent(SPOOF_VIDEO_STREAMS));
public static final BooleanSetting SPOOF_STREAMING_DATA_STATS_FOR_NERDS = new BooleanSetting("revanced_spoof_video_streams_stats_for_nerds", TRUE, parent(SPOOF_VIDEO_STREAMS));
public static final BooleanSetting SANITIZE_SHARING_LINKS = new BooleanSetting("revanced_sanitize_sharing_links", TRUE);
public static final BooleanSetting REPLACE_MUSIC_LINKS_WITH_YOUTUBE = new BooleanSetting("revanced_replace_music_with_youtube", FALSE);

View file

@ -9,7 +9,6 @@ public class YouTubeAndMusicSettings extends BaseSettings {
public static final StringSetting CUSTOM_FILTER_STRINGS = new StringSetting("revanced_custom_filter_strings", "", true, parent(CUSTOM_FILTER));
// Miscellaneous
public static final BooleanSetting DEBUG_PROTOBUFFER = new BooleanSetting("revanced_debug_protobuffer", FALSE, false,
"revanced_debug_protobuffer_user_dialog_message", parent(BaseSettings.DEBUG));
public static final BooleanSetting DEBUG_PROTOCOLBUFFER = new BooleanSetting("revanced_debug_protocolbuffer", FALSE, false,
"revanced_debug_protocolbuffer_user_dialog_message", parent(BaseSettings.DEBUG));
}

View file

@ -15,15 +15,16 @@ import android.widget.TextView;
import android.widget.Toolbar;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.ResourceType;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.settings.BaseActivityHook;
import app.revanced.extension.shared.ui.Dim;
import app.revanced.extension.shared.settings.BaseSettings;
@SuppressWarnings({"deprecation", "NewApi"})
@SuppressWarnings("deprecation")
@RequiresApi(api = Build.VERSION_CODES.O)
public class ToolbarPreferenceFragment extends AbstractPreferenceFragment {
/**

View file

@ -9,9 +9,34 @@ import java.util.Locale;
import java.util.Objects;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
@SuppressWarnings("ConstantLocale")
public enum ClientType {
/**
* Video not playable: Paid, Movie, Private, Age-restricted.
* Uses non-adaptive bitrate.
* AV1 codec available.
*/
ANDROID_REEL(
3,
"ANDROID",
"com.google.android.youtube",
Build.MANUFACTURER,
Build.MODEL,
"Android",
Build.VERSION.RELEASE,
String.valueOf(Build.VERSION.SDK_INT),
Build.ID,
"20.44.38",
// This client has been used by most open-source YouTube stream extraction tools since 2024, including NewPipe Extractor, SmartTube, and Grayjay.
// This client can log in, but if an access token is used in the request, GVS can more easily identify the request as coming from ReVanced.
// This means that the GVS server can strengthen its validation of the ANDROID_REEL client.
true,
true,
false,
"Android Reel"
),
/**
* Video not playable: Kids / Paid / Movie / Private / Age-restricted.
* This client can only be used when logged out.
@ -28,10 +53,10 @@ public enum ClientType {
// Android 12.1
"32",
"SQ3A.220605.009.A1",
"132.0.6808.3",
"1.61.48",
false,
false,
true,
"Android VR 1.61"
),
/**
@ -48,39 +73,12 @@ public enum ClientType {
ANDROID_VR_1_61_48.osVersion,
Objects.requireNonNull(ANDROID_VR_1_61_48.androidSdkVersion),
Objects.requireNonNull(ANDROID_VR_1_61_48.buildId),
"107.0.5284.2",
"1.43.32",
ANDROID_VR_1_61_48.useAuth,
ANDROID_VR_1_61_48.supportsMultiAudioTracks,
ANDROID_VR_1_61_48.usePlayerEndpoint,
"Android VR 1.43"
),
/**
* Video not playable: Paid / Movie / Private / Age-restricted.
* Note: The 'Authorization' key must be excluded from the header.
*
* According to TeamNewPipe in 2022, if the 'androidSdkVersion' field is missing,
* the GVS did not return a valid response:
* [NewPipe#8713 (comment)](https://github.com/TeamNewPipe/NewPipe/issues/8713#issuecomment-1207443550).
*
* According to the latest commit in yt-dlp, the GVS returns a valid response
* even if the 'androidSdkVersion' field is missing:
* [yt-dlp#14693](https://github.com/yt-dlp/yt-dlp/pull/14693).
*
* For some reason, PoToken is not required.
*/
ANDROID_NO_SDK(
3,
"ANDROID",
"",
"",
"",
Build.VERSION.RELEASE,
"20.05.46",
"com.google.android.youtube/20.05.46 (Linux; U; Android " + Build.VERSION.RELEASE + ") gzip",
false,
true,
"Android No SDK"
),
/**
* Cannot play livestreams and lacks HDR, but can play videos with music and labeled "for children".
* <a href="https://dumps.tadiphone.dev/dumps/google/barbet">Google Pixel 9 Pro Fold</a>
@ -95,10 +93,10 @@ public enum ClientType {
"15",
"35",
"AP3A.241005.015.A2",
"132.0.6779.0",
"23.47.101",
true,
false,
true,
"Android Studio"
),
/**
@ -114,32 +112,8 @@ public enum ClientType {
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Safari/605.1.15",
false,
false,
"visionOS"
),
/**
* The device machine id for the iPad 6th Gen (iPad7,6).
* AV1 hardware decoding is not supported.
* See [this GitHub Gist](https://gist.github.com/adamawolf/3048717) for more information.
*
* Based on Google's actions to date, PoToken may not be required on devices with very low specs.
* For example, suppose the User-Agent for a PlayStation 3 (with 256MB of RAM) is used.
* Accessing 'Web' (https://www.youtube.com) will redirect to 'TV' (https://www.youtube.com/tv).
* 'TV' target devices with very low specs, such as embedded devices, game consoles, and blu-ray players, so PoToken is not required.
*
* For this reason, the device machine id for the iPad 6th Gen (with 2GB of RAM),
* the lowest spec device capable of running iPadOS 17, was used.
*/
IPADOS(5,
"IOS",
"Apple",
"iPad7,6",
"iPadOS",
"17.7.10.21H450",
"19.22.3",
"com.google.ios.youtube/19.22.3 (iPad7,6; U; CPU iPadOS 17_7_10 like Mac OS X; " + Locale.getDefault() + ")",
false,
true,
"iPadOS"
"visionOS"
);
/**
@ -195,13 +169,6 @@ public enum ClientType {
@Nullable
private final String buildId;
/**
* Cronet release version, as found in decompiled client apk.
* Field is null if not applicable.
*/
@Nullable
private final String cronetVersion;
/**
* App version.
*/
@ -217,6 +184,11 @@ public enum ClientType {
*/
public final boolean supportsMultiAudioTracks;
/**
* If the client should use the player endpoint for stream extraction.
*/
public final boolean usePlayerEndpoint;
/**
* Friendly name displayed in stats for nerds.
*/
@ -234,10 +206,10 @@ public enum ClientType {
String osVersion,
@NonNull String androidSdkVersion,
@NonNull String buildId,
@NonNull String cronetVersion,
String clientVersion,
boolean useAuth,
boolean supportsMultiAudioTracks,
boolean usePlayerEndpoint,
String friendlyName) {
this.id = id;
this.clientName = clientName;
@ -248,21 +220,20 @@ public enum ClientType {
this.osVersion = osVersion;
this.androidSdkVersion = androidSdkVersion;
this.buildId = buildId;
this.cronetVersion = cronetVersion;
this.clientVersion = clientVersion;
this.useAuth = useAuth;
this.supportsMultiAudioTracks = supportsMultiAudioTracks;
this.usePlayerEndpoint = usePlayerEndpoint;
this.friendlyName = friendlyName;
Locale defaultLocale = Locale.getDefault();
this.userAgent = String.format("%s/%s (Linux; U; Android %s; %s; %s; Build/%s; Cronet/%s)",
this.userAgent = String.format("%s/%s (Linux; U; Android %s; %s; %s; Build/%s)",
packageName,
clientVersion,
osVersion,
defaultLocale,
deviceModel,
Objects.requireNonNull(buildId),
Objects.requireNonNull(cronetVersion)
buildId
);
Logger.printDebug(() -> "userAgent: " + this.userAgent);
}
@ -278,6 +249,7 @@ public enum ClientType {
String userAgent,
boolean useAuth,
boolean supportsMultiAudioTracks,
boolean usePlayerEndpoint,
String friendlyName) {
this.id = id;
this.clientName = clientName;
@ -289,10 +261,10 @@ public enum ClientType {
this.userAgent = userAgent;
this.useAuth = useAuth;
this.supportsMultiAudioTracks = supportsMultiAudioTracks;
this.usePlayerEndpoint = usePlayerEndpoint;
this.friendlyName = friendlyName;
this.packageName = null;
this.androidSdkVersion = null;
this.buildId = null;
this.cronetVersion = null;
}
}

View file

@ -5,7 +5,6 @@ import android.text.TextUtils;
import androidx.annotation.Nullable;
import java.nio.ByteBuffer;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@ -39,7 +38,7 @@ public class SpoofVideoStreamsPatch {
@Nullable
private static volatile AppLanguage languageOverride;
private static volatile ClientType preferredClient = ClientType.ANDROID_VR_1_43_32;
private static volatile ClientType preferredClient = ClientType.ANDROID_REEL;
/**
* @return If this patch was included during patching.
@ -250,7 +249,7 @@ public class SpoofVideoStreamsPatch {
* Called after {@link #fetchStreams(String, Map)}.
*/
@Nullable
public static ByteBuffer getStreamingData(String videoId) {
public static byte[] getStreamingData(String videoId) {
if (SPOOF_STREAMING_DATA) {
try {
StreamingDataRequest request = StreamingDataRequest.getRequestForVideoId(videoId);

View file

@ -15,13 +15,20 @@ import app.revanced.extension.shared.spoof.ClientType;
import app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch;
final class PlayerRoutes {
static final Route.CompiledRoute GET_STREAMING_DATA = new Route(
static final Route.CompiledRoute GET_PLAYER_STREAMING_DATA = new Route(
Route.Method.POST,
"player" +
"?fields=streamingData" +
"&alt=proto"
).compile();
static final Route.CompiledRoute GET_REEL_STREAMING_DATA = new Route(
Route.Method.POST,
"reel/reel_item_watch" +
"?fields=playerResponse.playabilityStatus,playerResponse.streamingData" +
"&alt=proto"
).compile();
private static final String YT_API_URL = "https://youtubei.googleapis.com/youtubei/v1/";
/**
@ -47,6 +54,7 @@ final class PlayerRoutes {
Locale streamLocale = language.getLocale();
JSONObject client = new JSONObject();
client.put("deviceMake", clientType.deviceMake);
client.put("deviceModel", clientType.deviceModel);
client.put("clientName", clientType.clientName);
@ -61,9 +69,19 @@ final class PlayerRoutes {
context.put("client", client);
innerTubeBody.put("context", context);
innerTubeBody.put("contentCheckOk", true);
innerTubeBody.put("racyCheckOk", true);
innerTubeBody.put("videoId", videoId);
if (clientType.usePlayerEndpoint) {
innerTubeBody.put("contentCheckOk", true);
innerTubeBody.put("racyCheckOk", true);
innerTubeBody.put("videoId", videoId);
} else {
JSONObject playerRequest = new JSONObject();
playerRequest.put("contentCheckOk", true);
playerRequest.put("racyCheckOk", true);
playerRequest.put("videoId", videoId);
innerTubeBody.put("playerRequest", playerRequest);
innerTubeBody.put("disablePlayerResponse", false);
}
} catch (JSONException e) {
Logger.printException(() -> "Failed to create innerTubeBody", e);
}

View file

@ -1,18 +1,17 @@
package app.revanced.extension.shared.spoof.requests;
import static app.revanced.extension.shared.ByteTrieSearch.convertStringsToBytes;
import static app.revanced.extension.shared.spoof.requests.PlayerRoutes.GET_STREAMING_DATA;
import static app.revanced.extension.shared.Utils.isNotEmpty;
import static app.revanced.extension.shared.spoof.requests.PlayerRoutes.GET_PLAYER_STREAMING_DATA;
import static app.revanced.extension.shared.spoof.requests.PlayerRoutes.GET_REEL_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;
@ -27,6 +26,11 @@ import java.util.concurrent.TimeoutException;
import app.revanced.extension.shared.ByteTrieSearch;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.innertube.PlayerResponseOuterClass;
import app.revanced.extension.shared.innertube.PlayerResponseOuterClass.PlayerResponse;
import app.revanced.extension.shared.innertube.PlayerResponseOuterClass.StreamingData;
import app.revanced.extension.shared.innertube.ReelItemWatchResponseOuterClass.ReelItemWatchResponse;
import app.revanced.extension.shared.requests.Route;
import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.spoof.ClientType;
@ -41,7 +45,7 @@ import app.revanced.extension.shared.spoof.ClientType;
*/
public class StreamingDataRequest {
private static volatile ClientType[] clientOrderToUse = ClientType.values();
private static volatile ClientType[] clientOrderToUse = ClientType.values();
public static void setClientOrderToUse(List<ClientType> availableClients, ClientType preferredClient) {
Objects.requireNonNull(preferredClient);
@ -111,7 +115,7 @@ public class StreamingDataRequest {
private final String videoId;
private final Future<ByteBuffer> future;
private final Future<byte[]> future;
private StreamingDataRequest(String videoId, Map<String, String> playerHeaders) {
Objects.requireNonNull(playerHeaders);
@ -134,6 +138,12 @@ public class StreamingDataRequest {
Logger.printInfo(() -> toastMessage, ex);
}
private static void handleDebugToast(String toastMessage, ClientType clientType) {
if (BaseSettings.DEBUG.get() && BaseSettings.DEBUG_TOAST_ON_ERROR.get()) {
Utils.showToastShort(String.format(toastMessage, clientType));
}
}
@Nullable
private static HttpURLConnection send(ClientType clientType,
String videoId,
@ -146,7 +156,10 @@ public class StreamingDataRequest {
final long startTime = System.currentTimeMillis();
try {
HttpURLConnection connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(GET_STREAMING_DATA, clientType);
Route.CompiledRoute route = clientType.usePlayerEndpoint ?
GET_PLAYER_STREAMING_DATA : GET_REEL_STREAMING_DATA;
HttpURLConnection connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(route, clientType);
connection.setConnectTimeout(HTTP_TIMEOUT_MILLISECONDS);
connection.setReadTimeout(HTTP_TIMEOUT_MILLISECONDS);
@ -203,7 +216,7 @@ public class StreamingDataRequest {
return null;
}
private static ByteBuffer fetch(String videoId, Map<String, String> playerHeaders) {
private static byte[] fetch(String videoId, Map<String, String> playerHeaders) {
final boolean debugEnabled = BaseSettings.DEBUG.get();
// Retry with different client if empty response body is received.
@ -214,33 +227,11 @@ public class StreamingDataRequest {
HttpURLConnection connection = send(clientType, videoId, playerHeaders, showErrorToast);
if (connection != null) {
try {
// gzip encoding doesn't response with content length (-1),
// but empty response body does.
if (connection.getContentLength() == 0) {
if (BaseSettings.DEBUG.get() && BaseSettings.DEBUG_TOAST_ON_ERROR.get()) {
Utils.showToastShort("Debug: Ignoring empty spoof stream client " + clientType);
}
} else {
try (InputStream inputStream = new BufferedInputStream(connection.getInputStream());
ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
byte[] playerResponseBuffer = buildPlayerResponseBuffer(clientType, connection);
if (playerResponseBuffer != null) {
lastSpoofedClientType = clientType;
byte[] buffer = new byte[2048];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) >= 0) {
baos.write(buffer, 0, bytesRead);
}
if (clientType == ClientType.ANDROID_CREATOR && liveStreamBufferSearch.matches(buffer)) {
Logger.printDebug(() -> "Skipping Android Studio as video is a livestream: " + videoId);
} else {
lastSpoofedClientType = clientType;
return ByteBuffer.wrap(baos.toByteArray());
}
}
}
} catch (IOException ex) {
Logger.printException(() -> "Fetch failed while processing response data", ex);
return playerResponseBuffer;
}
}
}
@ -250,12 +241,61 @@ public class StreamingDataRequest {
return null;
}
@Nullable
private static byte[] buildPlayerResponseBuffer(ClientType clientType,
HttpURLConnection connection) {
// gzip encoding doesn't response with content length (-1),
// but empty response body does.
if (connection.getContentLength() == 0) {
handleDebugToast("Debug: Ignoring empty spoof stream client (%s)", clientType);
return null;
}
try (InputStream inputStream = connection.getInputStream()) {
PlayerResponse playerResponse = clientType.usePlayerEndpoint
? PlayerResponse.parseFrom(inputStream)
: ReelItemWatchResponse.parseFrom(inputStream).getPlayerResponse();
var playabilityStatus = playerResponse.getPlayabilityStatus();
if (playabilityStatus.getStatus() != PlayerResponseOuterClass.Status.OK) {
handleDebugToast("Debug: Ignoring unplayable video (%s)", clientType);
String reason = playabilityStatus.getReason();
if (isNotEmpty(reason)) {
Logger.printDebug(() -> String.format("Debug: Ignoring unplayable video (%s), reason: %s", clientType, reason));
}
return null;
}
PlayerResponse.Builder responseBuilder = playerResponse.toBuilder();
if (!playerResponse.hasStreamingData()) {
handleDebugToast("Debug: Ignoring empty streaming data (%s)", clientType);
return null;
}
// Android Studio only supports the HLS protocol for live streams.
// HLS protocol can theoretically be played with ExoPlayer,
// but the related code has not yet been implemented.
// If DASH protocol is not available, the client will be skipped.
StreamingData streamingData = playerResponse.getStreamingData();
if (streamingData.getAdaptiveFormatsCount() == 0) {
handleDebugToast("Debug: Ignoring empty adaptiveFormat (%s)", clientType);
return null;
}
return responseBuilder.build().toByteArray();
} catch (IOException ex) {
Logger.printException(() -> "Failed to write player response to buffer array", ex);
return null;
}
}
public boolean fetchCompleted() {
return future.isDone();
}
@Nullable
public ByteBuffer getStream() {
public byte[] getStream() {
try {
return future.get(MAX_MILLISECONDS_TO_WAIT_FOR_FETCH, TimeUnit.MILLISECONDS);
} catch (TimeoutException ex) {

View file

@ -0,0 +1,55 @@
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
plugins {
kotlin("jvm")
alias(libs.plugins.protobuf)
alias(libs.plugins.shadow)
}
val shade: Configuration by configurations.creating {
configurations.getByName("compileClasspath").extendsFrom(this)
configurations.getByName("runtimeClasspath").extendsFrom(this)
}
dependencies {
compileOnly(libs.annotation)
compileOnly(libs.okhttp)
shade(libs.protobuf.javalite)
}
sourceSets {
// Make sure generated proto sources are compiled and end up in the shaded jar
main {
java.srcDir("$buildDir/generated/source/proto/main/java")
}
}
protobuf {
protoc {
artifact = libs.protobuf.protoc.get().toString()
}
generateProtoTasks {
all().forEach { task ->
task.builtins {
named("java") {
option("lite")
}
}
}
}
}
val shadowJar = tasks.named<ShadowJar>("shadowJar") {
configurations = listOf(shade)
relocate("com.google.protobuf", "app.revanced.com.google.protobuf")
}
configurations.named("runtimeElements") {
isCanBeConsumed = true
isCanBeResolved = false
outgoing.artifacts.clear()
outgoing.artifact(shadowJar)
}!!.let { artifacts { add(it.name, shadowJar) } }

View file

@ -0,0 +1,42 @@
syntax = "proto3";
package app.revanced.extension.shared.innertube;
option optimize_for = LITE_RUNTIME;
option java_package = "app.revanced.extension.shared.innertube";
message PlayerResponse {
oneof data {
PlayabilityStatus playability_status = 2;
StreamingData streaming_data = 4;
}
}
message PlayabilityStatus {
Status status = 1;
string reason = 2;
}
enum Status {
OK = 0;
ERROR = 1;
UNPLAYABLE = 2;
LOGIN_REQUIRED = 3;
CONTENT_CHECK_REQUIRED = 4;
AGE_CHECK_REQUIRED = 5;
LIVE_STREAM_OFFLINE = 6;
FULLSCREEN_ONLY = 7;
GL_PLAYBACK_REQUIRED = 8;
AGE_VERIFICATION_REQUIRED = 9;
}
message StreamingData {
repeated Format formats = 2;
repeated Format adaptiveFormats = 3;
string serverAbrStreamingUrl = 15;
}
message Format {
string url = 2;
string signatureCipher = 48;
}

View file

@ -0,0 +1,14 @@
syntax = "proto3";
import "app/revanced/extension/shared/innertube/player_response.proto";
package app.revanced.extension.shared.innertube;
option optimize_for = LITE_RUNTIME;
option java_package = "app.revanced.extension.shared.innertube";
message ReelItemWatchResponse {
oneof data {
PlayerResponse player_response = 4;
}
}

View file

@ -6,11 +6,6 @@ dependencies {
android {
defaultConfig {
minSdk = 21
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
minSdk = 24
}
}

View file

@ -7,11 +7,11 @@ android {
compileSdk = 34
defaultConfig {
minSdk = 21
minSdk = 24
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
}

View file

@ -3,3 +3,9 @@ dependencies {
compileOnly(project(":extensions:strava:stub"))
compileOnly(libs.okhttp)
}
android {
defaultConfig {
minSdk = 26
}
}

View file

@ -1,9 +1,7 @@
package app.revanced.extension.strava;
import android.annotation.SuppressLint;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
@ -28,7 +26,6 @@ import java.util.stream.Stream;
import app.revanced.extension.shared.Utils;
@SuppressLint("NewApi")
public final class AddMediaDownloadPatch {
public static final int ACTION_DOWNLOAD = -1;
public static final int ACTION_OPEN_LINK = -2;
@ -85,7 +82,7 @@ public final class AddMediaDownloadPatch {
} finally {
values.clear();
values.put(MediaStore.Images.Media.IS_PENDING, 0);
resolver.update(row, values, null);
resolver.update(row, values, null, null);
}
showInfoToast("yis_2024_local_save_image_success", "✔️");
} catch (IOException e) {
@ -151,7 +148,7 @@ public final class AddMediaDownloadPatch {
} finally {
values.clear();
values.put(MediaStore.Video.Media.IS_PENDING, 0);
resolver.update(row, values, null);
resolver.update(row, values, null, null);
}
showInfoToast("yis_2024_local_save_video_success", "✔️");
} catch (IOException e) {

View file

@ -1,7 +1,5 @@
package app.revanced.extension.strava;
import android.annotation.SuppressLint;
import com.strava.modularframework.data.Destination;
import com.strava.modularframework.data.GenericLayoutModule;
import com.strava.modularframework.data.GenericModuleField;
@ -21,7 +19,6 @@ import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@SuppressLint("NewApi")
public class HideDistractionsPatch {
public static boolean upselling;
public static boolean promo;

View file

@ -7,6 +7,6 @@ android {
compileSdk = 34
defaultConfig {
minSdk = 21
minSdk = 26
}
}

View file

@ -4,3 +4,9 @@ dependencies {
compileOnly(libs.annotation)
compileOnly(libs.okhttp)
}
android {
defaultConfig {
minSdk = 23
}
}

View file

@ -8,9 +8,4 @@ android {
defaultConfig {
minSdk = 22
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
}

View file

@ -20,7 +20,7 @@ import java.lang.reflect.InvocationTargetException;
/**
* Hooks AdPersonalizationActivity to inject a custom {@link TikTokPreferenceFragment}.
*/
@SuppressWarnings({"deprecation", "NewApi", "unused"})
@SuppressWarnings({"deprecation", "unused"})
public class TikTokActivityHook {
public static Object createSettingsEntry(String entryClazzName, String entryInfoClazzName) {
try {

View file

@ -1,3 +1,9 @@
dependencies {
compileOnly(libs.appcompat)
}
android {
defaultConfig {
minSdk = 22
}
}

View file

@ -11,9 +11,4 @@ android {
defaultConfig {
minSdk = 21
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
}

View file

@ -22,7 +22,7 @@ import tv.twitch.android.settings.SettingsActivity;
/**
* Hooks AppCompatActivity to inject a custom {@link TwitchPreferenceFragment}.
*/
@SuppressWarnings({"deprecation", "NewApi", "unused"})
@SuppressWarnings({"deprecation", "unused"})
public class TwitchActivityHook {
private static final int REVANCED_SETTINGS_MENU_ITEM_ID = 0x7;
private static final String EXTRA_REVANCED_SETTINGS = "app.revanced.twitch.settings";

View file

@ -1,3 +1,9 @@
dependencies {
compileOnly(project(":extensions:shared:library"))
}
android {
defaultConfig {
minSdk = 26
}
}

View file

@ -238,8 +238,8 @@ public final class AlternativeThumbnailsPatch {
// See https://github.com/ajayyy/DeArrowThumbnailCache/blob/29eb4359ebdf823626c79d944a901492d760bbbc/app.py#L29.
return dearrowAPIURI
.buildUpon()
.appendQueryParameter("videoId", videoId)
.appendQueryParameter("redirectURL", fallbackURL)
.appendQueryParameter("videoID", videoId)
.appendQueryParameter("redirectUrl", fallbackURL)
.build()
.toString();
}

View file

@ -14,7 +14,9 @@ import android.widget.LinearLayout;
import android.widget.TextView;
import app.revanced.extension.shared.ui.CustomDialog;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.IOException;
import java.net.HttpURLConnection;
@ -29,9 +31,20 @@ import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings("unused")
public final class AnnouncementsPatch {
private static final String[] ANNOUNCEMENT_TAGS = {
"\uD83C\uDF9E\uFE0F YouTube",
};
private AnnouncementsPatch() {
}
private static boolean hasSupportedTag(String wrapperTag) {
if (wrapperTag == null) return false;
for (var tag : ANNOUNCEMENT_TAGS) if (tag.equals(wrapperTag)) return true;
return false;
}
private static boolean isLatestAlready() throws IOException {
HttpURLConnection connection =
AnnouncementsRoutes.getAnnouncementsConnectionFromRoute(GET_LATEST_ANNOUNCEMENT_IDS);
@ -56,19 +69,30 @@ public final class AnnouncementsPatch {
var jsonString = Requester.parseStringAndDisconnect(connection);
// Parse the ID. Fall-back to raw string if it fails.
int id = Settings.ANNOUNCEMENT_LAST_ID.defaultValue;
var id = -1;
try {
final var announcementIDs = new JSONArray(jsonString);
if (announcementIDs.length() == 0) return true;
id = announcementIDs.getJSONObject(0).getInt("id");
final var announcementIDTagPairs = new JSONArray(jsonString);
if (announcementIDTagPairs.length() == 0) return true;
JSONObject latest = null;
for (int i = 0, entryCount = announcementIDTagPairs.length(); i < entryCount; i++) {
var pair = announcementIDTagPairs.optJSONObject(i);
if (pair != null && hasSupportedTag(pair.optString("tag", null))) {
latest = pair;
break;
}
}
if (latest == null || latest.isNull("id")) return true;
id = latest.getInt("id");
} catch (Throwable ex) {
Logger.printException(() -> "Failed to parse announcement ID", ex);
return true;
}
// Do not show the announcement, if the last announcement id is the same as the current one.
return Settings.ANNOUNCEMENT_LAST_ID.get() == id;
return Settings.ANNOUNCEMENT_LAST_ID.get().equals(id);
}
public static void showAnnouncement(final Activity context) {
@ -95,7 +119,22 @@ public final class AnnouncementsPatch {
LocalDateTime archivedAt = LocalDateTime.MAX;
Level level = Level.INFO;
try {
final var announcement = new JSONArray(jsonString).getJSONObject(0);
final var announcements = new JSONArray(jsonString);
JSONObject latestAnnouncement = null;
for (int i = 0, entryCount = announcements.length(); i < entryCount; i++) {
var announcementTagPair = announcements.optJSONObject(i);
if (announcementTagPair != null && hasSupportedTag(announcementTagPair.optString("tag", null))) {
latestAnnouncement = announcementTagPair;
break;
}
}
if (latestAnnouncement == null || latestAnnouncement.isNull("announcement")) {
Logger.printDebug(() -> "No YouTube announcement found in latest announcements response");
return;
}
final var announcement = latestAnnouncement.getJSONObject("announcement");
id = announcement.getInt("id");
title = announcement.getString("title");

View file

@ -9,9 +9,9 @@ import java.net.HttpURLConnection;
import static app.revanced.extension.shared.requests.Route.Method.GET;
public class AnnouncementsRoutes {
private static final String ANNOUNCEMENTS_PROVIDER = "https://api.revanced.app/v4";
public static final Route GET_LATEST_ANNOUNCEMENT_IDS = new Route(GET, "/announcements/latest/id?tag=\uD83C\uDF9E\uFE0F%20YouTube");
public static final Route GET_LATEST_ANNOUNCEMENTS = new Route(GET, "/announcements/latest?tag=\uD83C\uDF9E\uFE0F%20YouTube");
private static final String ANNOUNCEMENTS_PROVIDER = "https://api.revanced.app/v5";
public static final Route GET_LATEST_ANNOUNCEMENT_IDS = new Route(GET, "/announcements/latest/id");
public static final Route GET_LATEST_ANNOUNCEMENTS = new Route(GET, "/announcements/latest");
private AnnouncementsRoutes() {
}

View file

@ -1,10 +1,9 @@
package app.revanced.extension.youtube.patches.spoof;
import static app.revanced.extension.shared.spoof.ClientType.ANDROID_CREATOR;
import static app.revanced.extension.shared.spoof.ClientType.ANDROID_NO_SDK;
import static app.revanced.extension.shared.spoof.ClientType.ANDROID_VR_1_43_32;
import static app.revanced.extension.shared.spoof.ClientType.ANDROID_VR_1_61_48;
import static app.revanced.extension.shared.spoof.ClientType.IPADOS;
import static app.revanced.extension.shared.spoof.ClientType.ANDROID_REEL;
import static app.revanced.extension.shared.spoof.ClientType.VISIONOS;
import java.util.List;
@ -44,11 +43,11 @@ public class SpoofVideoStreamsPatch {
}
List<ClientType> availableClients = List.of(
VISIONOS,
ANDROID_CREATOR,
ANDROID_REEL,
ANDROID_VR_1_43_32,
ANDROID_NO_SDK,
IPADOS);
VISIONOS,
ANDROID_CREATOR
);
app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch.setClientsToUse(
availableClients, client);

View file

@ -1,7 +1,9 @@
package app.revanced.extension.youtube.patches.theme;
import android.content.res.ColorStateList;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
@ -10,7 +12,6 @@ import androidx.annotation.Nullable;
import app.revanced.extension.shared.ResourceType;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.settings.BaseSettings;
/**
* Dynamic drawable that is either the regular or bolded ReVanced preference icon.
@ -82,4 +83,19 @@ public class ReVancedSettingsIconDynamicDrawable extends Drawable {
super.onBoundsChange(bounds);
icon.setBounds(bounds);
}
@Override
public void setTint(int tintColor) {
icon.setTint(tintColor);
}
@Override
public void setTintList(@Nullable ColorStateList tint) {
icon.setTintList(tint);
}
@Override
public void setTintMode(@Nullable PorterDuff.Mode tintMode) {
icon.setTintMode(tintMode);
}
}

View file

@ -392,7 +392,7 @@ public class Settings extends YouTubeAndMusicSettings {
public static final BooleanSetting EXTERNAL_BROWSER = new BooleanSetting("revanced_external_browser", TRUE, true);
public static final BooleanSetting SPOOF_DEVICE_DIMENSIONS = new BooleanSetting("revanced_spoof_device_dimensions", FALSE, true,
"revanced_spoof_device_dimensions_user_dialog_message");
public static final EnumSetting<ClientType> SPOOF_VIDEO_STREAMS_CLIENT_TYPE = new EnumSetting<>("revanced_spoof_video_streams_client_type", ClientType.ANDROID_VR_1_43_32, true, parent(SPOOF_VIDEO_STREAMS));
public static final EnumSetting<ClientType> SPOOF_VIDEO_STREAMS_CLIENT_TYPE = new EnumSetting<>("revanced_spoof_video_streams_client_type", ClientType.ANDROID_REEL, true, parent(SPOOF_VIDEO_STREAMS));
public static final BooleanSetting SPOOF_VIDEO_STREAMS_AV1 = new BooleanSetting("revanced_spoof_video_streams_av1", FALSE, true,
"revanced_spoof_video_streams_av1_user_dialog_message", new SpoofClientAv1Availability());

View file

@ -19,7 +19,7 @@ import app.revanced.extension.shared.spoof.ClientType;
import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings({"deprecation", "unused"})
public class SpoofStreamingDataSideEffectsPreference extends Preference {
public class SpoofVideoStreamsSideEffectsPreference extends Preference {
@Nullable
private ClientType currentClientType;
@ -33,19 +33,19 @@ public class SpoofStreamingDataSideEffectsPreference extends Preference {
Utils.runOnMainThread(this::updateUI);
};
public SpoofStreamingDataSideEffectsPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
public SpoofVideoStreamsSideEffectsPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public SpoofStreamingDataSideEffectsPreference(Context context, AttributeSet attrs, int defStyleAttr) {
public SpoofVideoStreamsSideEffectsPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public SpoofStreamingDataSideEffectsPreference(Context context, AttributeSet attrs) {
public SpoofVideoStreamsSideEffectsPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public SpoofStreamingDataSideEffectsPreference(Context context) {
public SpoofVideoStreamsSideEffectsPreference(Context context) {
super(context);
}
@ -88,27 +88,23 @@ public class SpoofStreamingDataSideEffectsPreference extends Preference {
+ '\n' + str("revanced_spoof_video_streams_about_no_stable_volume")
+ '\n' + str("revanced_spoof_video_streams_about_no_av1")
+ '\n' + str("revanced_spoof_video_streams_about_no_force_original_audio");
case ANDROID_REEL ->
summary = str("revanced_spoof_video_streams_about_playback_failure");
// VR 1.61 is not exposed in the UI and should never be reached here.
case ANDROID_VR_1_43_32, ANDROID_VR_1_61_48 ->
summary = str("revanced_spoof_video_streams_about_no_audio_tracks")
+ '\n' + str("revanced_spoof_video_streams_about_no_stable_volume");
case ANDROID_NO_SDK ->
summary = str("revanced_spoof_video_streams_about_playback_failure");
case IPADOS ->
summary = str("revanced_spoof_video_streams_about_playback_failure")
+ '\n' + str("revanced_spoof_video_streams_about_no_av1");
case VISIONOS ->
summary = str("revanced_spoof_video_streams_about_experimental")
+ '\n' + str("revanced_spoof_video_streams_about_no_audio_tracks")
+ '\n' + str("revanced_spoof_video_streams_about_no_av1");
+ '\n' + str("revanced_spoof_video_streams_about_no_stable_volume");
case VISIONOS -> summary = str("revanced_spoof_video_streams_about_experimental")
+ '\n' + str("revanced_spoof_video_streams_about_playback_failure")
+ '\n' + str("revanced_spoof_video_streams_about_no_audio_tracks")
+ '\n' + str("revanced_spoof_video_streams_about_no_av1");
default -> Logger.printException(() -> "Unknown client: " + clientType);
}
// Only iPadOS can play children videos in incognito, but it commonly fails at 1 minute
// or doesn't start playback at all. List the side effect for other clients
// since they will fall over to iPadOS.
if (clientType != ClientType.IPADOS && clientType != ClientType.ANDROID_NO_SDK) {
summary += '\n' + str("revanced_spoof_video_streams_about_kids_videos");
// Only Android Reel and Android VR supports 360° VR immersive mode.
if (!clientType.name().startsWith("ANDROID_VR") && clientType != ClientType.ANDROID_REEL) {
summary += '\n' + str("revanced_spoof_video_streams_about_no_immersive_mode");
}
// Use better formatting for bullet points.

View file

@ -3,7 +3,6 @@ package app.revanced.extension.youtube.sponsorblock;
import static app.revanced.extension.shared.StringRef.str;
import static app.revanced.extension.youtube.sponsorblock.objects.CategoryBehaviour.SKIP_AUTOMATICALLY;
import android.annotation.SuppressLint;
import android.app.Dialog;
import android.content.Context;
import android.graphics.Canvas;
@ -51,7 +50,6 @@ import kotlin.Unit;
* <p>
* Class is not thread safe. All methods must be called on the main thread unless otherwise specified.
*/
@SuppressLint("NewApi")
public class SegmentPlaybackController {
/**

View file

@ -26,7 +26,6 @@ import app.revanced.extension.youtube.sponsorblock.objects.CategoryBehaviour;
import app.revanced.extension.youtube.sponsorblock.objects.SegmentCategory;
import app.revanced.extension.youtube.sponsorblock.ui.SponsorBlockPreferenceGroup;
@SuppressWarnings("NewApi")
public class SponsorBlockSettings {
/**
* Minimum length an SB user ID must be, as set by SB API.