diff --git a/.github/workflows/pull_strings.yml b/.github/workflows/pull_strings.yml index 8fef9fe309..f2da51ec74 100644 --- a/.github/workflows/pull_strings.yml +++ b/.github/workflows/pull_strings.yml @@ -32,6 +32,7 @@ jobs: - name: Process strings run: | + chmod -R 777 patches/src/main/resources ./gradlew processStringsFromCrowdin env: ORG_GRADLE_PROJECT_githubPackagesUsername: ${{ github.actor }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 822edfd6d9..bfebce7e69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,47 @@ +# [6.1.0](https://github.com/ReVanced/revanced-patches/compare/v6.0.1...v6.1.0) (2026-03-18) + + +### Bug Fixes + +* **Export internal data documents provider:** Correct S_IFLNK constant and symlink detection mask ([#6819](https://github.com/ReVanced/revanced-patches/issues/6819)) ([252617b](https://github.com/ReVanced/revanced-patches/commit/252617b8dd3f24e1ff9a04ba1d91b43dc29bd757)) +* **YouTube - Custom branding:** Fix double icons and change default branding to ReVanced ([#6806](https://github.com/ReVanced/revanced-patches/issues/6806)) ([e51c529](https://github.com/ReVanced/revanced-patches/commit/e51c5292c171325e7cfa0f5ee85474d9b3961a34)) + + +### Features + +* Add `Spoof root of trust` and `Spoof keystore security level` patch ([#6751](https://github.com/ReVanced/revanced-patches/issues/6751)) ([4bc8c7c](https://github.com/ReVanced/revanced-patches/commit/4bc8c7c0f60a095533f07dc281f0320f8eb22f3c)) +* **Announcements:** Support ReVanced API v5 announcements ([a05386e](https://github.com/ReVanced/revanced-patches/commit/a05386e8bc24c085b5c74f3674c402c5dd5ad468)) +* Change contact email in patches about ([df1c3a4](https://github.com/ReVanced/revanced-patches/commit/df1c3a4a70fd2595d77b539299f1f7301bc60d24)) +* **Instagram:** Add `Enable location sticker redesign` patch ([#6808](https://github.com/ReVanced/revanced-patches/issues/6808)) ([4b699da](https://github.com/ReVanced/revanced-patches/commit/4b699da220e5d1527c390792b6228e2d9cffedb7)) +* **Spoof video streams:** Add Android Reel client to fix playback issues ([#6830](https://github.com/ReVanced/revanced-patches/issues/6830)) ([4b6c3e3](https://github.com/ReVanced/revanced-patches/commit/4b6c3e312328fbf6a1c7065e27d8ff04573e58be)) + +# [6.1.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v6.1.0-dev.3...v6.1.0-dev.4) (2026-03-18) + + +### Bug Fixes + +* **YouTube - Custom branding:** Fix double icons and change default branding to ReVanced ([#6806](https://github.com/ReVanced/revanced-patches/issues/6806)) ([e51c529](https://github.com/ReVanced/revanced-patches/commit/e51c5292c171325e7cfa0f5ee85474d9b3961a34)) + + +### Features + +* Add `Spoof root of trust` and `Spoof keystore security level` patch ([#6751](https://github.com/ReVanced/revanced-patches/issues/6751)) ([4bc8c7c](https://github.com/ReVanced/revanced-patches/commit/4bc8c7c0f60a095533f07dc281f0320f8eb22f3c)) +* **Instagram:** Add `Enable location sticker redesign` patch ([#6808](https://github.com/ReVanced/revanced-patches/issues/6808)) ([4b699da](https://github.com/ReVanced/revanced-patches/commit/4b699da220e5d1527c390792b6228e2d9cffedb7)) + +# [6.1.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v6.1.0-dev.2...v6.1.0-dev.3) (2026-03-18) + + +### Features + +* **Spoof video streams:** Add Android Reel client to fix playback issues ([#6830](https://github.com/ReVanced/revanced-patches/issues/6830)) ([4b6c3e3](https://github.com/ReVanced/revanced-patches/commit/4b6c3e312328fbf6a1c7065e27d8ff04573e58be)) + +# [6.1.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v6.1.0-dev.1...v6.1.0-dev.2) (2026-03-17) + + +### Features + +* **Announcements:** Support ReVanced API v5 announcements ([a05386e](https://github.com/ReVanced/revanced-patches/commit/a05386e8bc24c085b5c74f3674c402c5dd5ad468)) + # [6.1.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v6.0.2-dev.1...v6.1.0-dev.1) (2026-03-16) diff --git a/extensions/all/misc/disable-play-integrity/src/main/java/app/revanced/extension/playintegrity/DisablePlayIntegrityPatch.java b/extensions/all/misc/disable-play-integrity/src/main/java/app/revanced/extension/play/DisablePlayIntegrityPatch.java similarity index 92% rename from extensions/all/misc/disable-play-integrity/src/main/java/app/revanced/extension/playintegrity/DisablePlayIntegrityPatch.java rename to extensions/all/misc/disable-play-integrity/src/main/java/app/revanced/extension/play/DisablePlayIntegrityPatch.java index a27e56be95..4dd09f693f 100644 --- a/extensions/all/misc/disable-play-integrity/src/main/java/app/revanced/extension/playintegrity/DisablePlayIntegrityPatch.java +++ b/extensions/all/misc/disable-play-integrity/src/main/java/app/revanced/extension/play/DisablePlayIntegrityPatch.java @@ -1,4 +1,4 @@ -package app.revanced.extension.playintegrity; +package app.revanced.extension.play; import android.content.Context; import android.content.Intent; diff --git a/extensions/music/src/main/java/app/revanced/extension/music/patches/spoof/SpoofVideoStreamsPatch.java b/extensions/music/src/main/java/app/revanced/extension/music/patches/spoof/SpoofVideoStreamsPatch.java index ade26a30fb..46c85a8edd 100644 --- a/extensions/music/src/main/java/app/revanced/extension/music/patches/spoof/SpoofVideoStreamsPatch.java +++ b/extensions/music/src/main/java/app/revanced/extension/music/patches/spoof/SpoofVideoStreamsPatch.java @@ -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 availableClients = List.of( + ANDROID_REEL, ANDROID_VR_1_43_32, - ANDROID_NO_SDK, VISIONOS, ANDROID_VR_1_61_48 ); diff --git a/extensions/music/src/main/java/app/revanced/extension/music/settings/Settings.java b/extensions/music/src/main/java/app/revanced/extension/music/settings/Settings.java index b2f61541a4..7decd29b8a 100644 --- a/extensions/music/src/main/java/app/revanced/extension/music/settings/Settings.java +++ b/extensions/music/src/main/java/app/revanced/extension/music/settings/Settings.java @@ -35,7 +35,7 @@ public class Settings extends YouTubeAndMusicSettings { // Miscellaneous public static final EnumSetting 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); } diff --git a/extensions/shared/library/build.gradle.kts b/extensions/shared/library/build.gradle.kts index 100de7ae14..8215e513ad 100644 --- a/extensions/shared/library/build.gradle.kts +++ b/extensions/shared/library/build.gradle.kts @@ -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")) } diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/CustomBrandingPatch.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/CustomBrandingPatch.java index 0dc411f8b0..d13513e2df 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/CustomBrandingPatch.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/CustomBrandingPatch.java @@ -114,7 +114,7 @@ public class CustomBrandingPatch { /** * Injection point. - * + *

* 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; } /** diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/BaseSettings.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/BaseSettings.java index 2a3aef407f..5fc4418366 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/BaseSettings.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/BaseSettings.java @@ -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); diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/ClientType.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/ClientType.java index 39076b562d..9abd430719 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/ClientType.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/ClientType.java @@ -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". * Google Pixel 9 Pro Fold @@ -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; } } diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/SpoofVideoStreamsPatch.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/SpoofVideoStreamsPatch.java index 38fbac9938..0c861510fe 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/SpoofVideoStreamsPatch.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/SpoofVideoStreamsPatch.java @@ -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); diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/requests/PlayerRoutes.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/requests/PlayerRoutes.java index 31e3f03034..959048d1e2 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/requests/PlayerRoutes.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/requests/PlayerRoutes.java @@ -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); } diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/requests/StreamingDataRequest.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/requests/StreamingDataRequest.java index 8eb1eaaab5..fb8a8e79e8 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/requests/StreamingDataRequest.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/requests/StreamingDataRequest.java @@ -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 availableClients, ClientType preferredClient) { Objects.requireNonNull(preferredClient); @@ -111,7 +115,7 @@ public class StreamingDataRequest { private final String videoId; - private final Future future; + private final Future future; private StreamingDataRequest(String videoId, Map 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 playerHeaders) { + private static byte[] fetch(String videoId, Map 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) { diff --git a/extensions/shared/protobuf/build.gradle.kts b/extensions/shared/protobuf/build.gradle.kts new file mode 100644 index 0000000000..be3f7f6a08 --- /dev/null +++ b/extensions/shared/protobuf/build.gradle.kts @@ -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") { + 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) } } + diff --git a/extensions/shared/protobuf/src/main/proto/app/revanced/extension/shared/innertube/player_response.proto b/extensions/shared/protobuf/src/main/proto/app/revanced/extension/shared/innertube/player_response.proto new file mode 100644 index 0000000000..29f1a6d9bd --- /dev/null +++ b/extensions/shared/protobuf/src/main/proto/app/revanced/extension/shared/innertube/player_response.proto @@ -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; +} diff --git a/extensions/shared/protobuf/src/main/proto/app/revanced/extension/shared/innertube/reel_item_watch_response.proto b/extensions/shared/protobuf/src/main/proto/app/revanced/extension/shared/innertube/reel_item_watch_response.proto new file mode 100644 index 0000000000..e74f142533 --- /dev/null +++ b/extensions/shared/protobuf/src/main/proto/app/revanced/extension/shared/innertube/reel_item_watch_response.proto @@ -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; + } +} diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/announcements/AnnouncementsPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/announcements/AnnouncementsPatch.java index 1212416069..8e24d65ec5 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/announcements/AnnouncementsPatch.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/announcements/AnnouncementsPatch.java @@ -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"); diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/announcements/requests/AnnouncementsRoutes.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/announcements/requests/AnnouncementsRoutes.java index d89a2d3403..aa63ae864c 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/announcements/requests/AnnouncementsRoutes.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/announcements/requests/AnnouncementsRoutes.java @@ -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() { } diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/spoof/SpoofVideoStreamsPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/spoof/SpoofVideoStreamsPatch.java index 2bf442a937..21819a2828 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/spoof/SpoofVideoStreamsPatch.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/spoof/SpoofVideoStreamsPatch.java @@ -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 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); diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/SpoofStreamingDataSideEffectsPreference.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/SpoofVideoStreamsSideEffectsPreference.java similarity index 75% rename from extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/SpoofStreamingDataSideEffectsPreference.java rename to extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/SpoofVideoStreamsSideEffectsPreference.java index 86802ee200..1faaaf0a3e 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/SpoofStreamingDataSideEffectsPreference.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/SpoofVideoStreamsSideEffectsPreference.java @@ -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. diff --git a/gradle.properties b/gradle.properties index 655da7c79e..d65f6857e0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,4 +4,4 @@ org.gradle.parallel = true android.useAndroidX = true android.uniquePackageNames = false kotlin.code.style = official -version = 6.1.0-dev.1 +version = 6.1.0 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bea84117e6..5db73fbcee 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,6 +10,10 @@ okhttp = "5.3.2" retrofit = "3.0.0" guava = "33.5.0-jre" apksig = "9.0.1" +# TODO: Adjust once https://github.com/google/protobuf-gradle-plugin/pull/797 is merged. +protobuf = "master-SNAPSHOT" +protoc = "4.34.0" +shadow = "9.4.0" [libraries] annotation = { module = "androidx.annotation:annotation", version.ref = "annotation" } @@ -18,6 +22,10 @@ okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } guava = { module = "com.google.guava:guava", version.ref = "guava" } apksig = { group = "com.android.tools.build", name = "apksig", version.ref = "apksig" } +protobuf-javalite = { module = "com.google.protobuf:protobuf-javalite", version.ref = "protoc" } +protobuf-protoc = { module = "com.google.protobuf:protoc", version.ref = "protoc" } [plugins] android-library = { id = "com.android.library" } +protobuf = { id = "com.google.protobuf", version.ref = "protobuf" } +shadow = { id = "com.gradleup.shadow", version.ref = "shadow" } diff --git a/patches/api/patches.api b/patches/api/patches.api index be251e84a5..4a8ed81323 100644 --- a/patches/api/patches.api +++ b/patches/api/patches.api @@ -89,10 +89,14 @@ public final class app/revanced/patches/all/misc/packagename/ChangePackageNamePa public static final fun setOrGetFallbackPackageName (Ljava/lang/String;)Ljava/lang/String; } -public final class app/revanced/patches/all/misc/playintegrity/DisablePlayIntegrityKt { +public final class app/revanced/patches/all/misc/play/DisablePlayIntegrityKt { public static final fun getDisablePlayIntegrityPatch ()Lapp/revanced/patcher/patch/Patch; } +public final class app/revanced/patches/all/misc/play/SpoofPlayAgeSignalsKt { + public static final fun getSpoofPlayAgeSignalsPatch ()Lapp/revanced/patcher/patch/Patch; +} + public final class app/revanced/patches/all/misc/resources/AddResourcesPatchKt { public static final fun addResource (Ljava/lang/String;Lapp/revanced/util/resource/BaseResource;)Z public static final fun addResources (Lapp/revanced/patcher/patch/Patch;Lkotlin/jvm/functions/Function1;)Z @@ -125,6 +129,14 @@ public final class app/revanced/patches/all/misc/spoof/EnableRomSignatureSpoofin public static final fun getEnableROMSignatureSpoofingPatch ()Lapp/revanced/patcher/patch/Patch; } +public final class app/revanced/patches/all/misc/spoof/SpoofKeystoreSecurityLevelPatchKt { + public static final fun getSpoofKeystoreSecurityLevelPatch ()Lapp/revanced/patcher/patch/Patch; +} + +public final class app/revanced/patches/all/misc/spoof/SpoofRootOfTrustPatchKt { + public static final fun getSpoofRootOfTrustPatch ()Lapp/revanced/patcher/patch/Patch; +} + public final class app/revanced/patches/all/misc/targetSdk/SetTargetSdkVersion34Kt { public static final fun getSetTargetSDKVersion34Patch ()Lapp/revanced/patcher/patch/Patch; } @@ -365,6 +377,10 @@ public final class app/revanced/patches/instagram/story/flipping/DisableStoryAut public static final fun getDisableStoryAutoFlippingPatch ()Lapp/revanced/patcher/patch/Patch; } +public final class app/revanced/patches/instagram/story/locationsticker/EnableLocationStickerRedesignPatchKt { + public static final fun getEnableLocationStickerRedesignPatch ()Lapp/revanced/patcher/patch/Patch; +} + public final class app/revanced/patches/irplus/ad/RemoveAdsPatchKt { public static final fun getRemoveAdsPatch ()Lapp/revanced/patcher/patch/Patch; } diff --git a/patches/src/main/kotlin/app/revanced/patches/all/misc/connectivity/telephony/sim/spoof/SpoofSimProviderPatch.kt b/patches/src/main/kotlin/app/revanced/patches/all/misc/connectivity/telephony/sim/spoof/SpoofSimProviderPatch.kt index a7dfdf7bf4..cb5723de76 100644 --- a/patches/src/main/kotlin/app/revanced/patches/all/misc/connectivity/telephony/sim/spoof/SpoofSimProviderPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/all/misc/connectivity/telephony/sim/spoof/SpoofSimProviderPatch.kt @@ -6,7 +6,8 @@ import app.revanced.patcher.extensions.replaceInstruction import app.revanced.patcher.patch.bytecodePatch import app.revanced.patcher.patch.intOption import app.revanced.patcher.patch.stringOption -import app.revanced.patches.all.misc.transformation.transformInstructionsPatch +import app.revanced.util.forEachInstructionAsSequence +import com.android.tools.smali.dexlib2.iface.instruction.Instruction import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction import com.android.tools.smali.dexlib2.iface.reference.MethodReference @@ -33,7 +34,8 @@ val spoofSIMProviderPatch = bytecodePatch( validator = { it: String? -> it == null || it.uppercase() in countries.values }, ) - fun isMccMncValid(it: Int?): Boolean = it == null || (it >= 10000 && it <= 999999) + fun isMccMncValid(it: Int?) = it == null || (it in 10000..999999) + fun isNumericValid(it: String?, length: Int) = it.isNullOrBlank() || it.equals("random", true) || it.length == length val networkCountryIso by isoCountryPatchOption("Network ISO country code") @@ -61,46 +63,119 @@ val spoofSIMProviderPatch = bytecodePatch( description = "The full name of the SIM operator.", ) + val imei by stringOption( + name = "IMEI value", + description = "15-digit IMEI to spoof, blank to skip, or 'random'.", + validator = { isNumericValid(it, 15) }, + ) + + val meid by stringOption( + name = "MEID value", + description = "14-char hex MEID to spoof, blank to skip, or 'random'.", + validator = { isNumericValid(it, 14) }, + ) + + val imsi by stringOption( + name = "IMSI (Subscriber ID)", + description = "15-digit IMSI to spoof, blank to skip, or 'random'.", + validator = { isNumericValid(it, 15) }, + ) + + val iccid by stringOption( + name = "ICCID (SIM Serial)", + description = "19-digit ICCID to spoof, blank to skip, or 'random'.", + validator = { isNumericValid(it, 19) }, + ) + + val phone by stringOption( + name = "Phone number", + description = "Phone number to spoof, blank to skip, or 'random'.", + validator = { it.isNullOrBlank() || it.equals("random", ignoreCase = true) || it.startsWith("+") }, + ) + dependsOn( - transformInstructionsPatch( - filterMap = { _, _, instruction, instructionIndex -> - if (instruction !is ReferenceInstruction) return@transformInstructionsPatch null + bytecodePatch { + apply { + fun generateRandomNumeric(length: Int) = (1..length).map { ('0'..'9').random() }.joinToString("") - val reference = instruction.reference as? MethodReference ?: return@transformInstructionsPatch null - - val match = MethodCall.entries.firstOrNull { search -> - MethodUtil.methodSignaturesMatch(reference, search.reference) - } ?: return@transformInstructionsPatch null - - val replacement = when (match) { - MethodCall.NetworkCountryIso -> networkCountryIso?.lowercase() - MethodCall.NetworkOperator -> networkOperator?.toString() - MethodCall.NetworkOperatorName -> networkOperatorName - MethodCall.SimCountryIso -> simCountryIso?.lowercase() - MethodCall.SimOperator -> simOperator?.toString() - MethodCall.SimOperatorName -> simOperatorName + fun String?.computeSpoof(randomizer: () -> String): String? { + if (this.isNullOrBlank()) return null + if (this.equals("random", ignoreCase = true)) return randomizer() + return this } - replacement?.let { instructionIndex to it } - }, - transform = ::transformMethodCall, - ), + + // Calculate the Luhn checksum (mod 10) to generate a valid 15th digit, standard for IMEI numbers. + // Structure of an IMEI is as follows: + // TAC (Type Allocation Code): First 8 digits (e.g., "86" + 6 digits) + // SNR (Serial Number): Next 6 digits + // CD (Check Digit): The 15th digit + val computedImei = imei.computeSpoof { + val prefix = "86" + generateRandomNumeric(12) + + val sum = prefix.mapIndexed { i, c -> + var d = c.digitToInt() + // Double every second digit (index 1, 3, 5...). + if (i % 2 != 0) { + d *= 2 + // If result is two digits (e.g. 14), sum them (1+4=5). + // This is mathematically equivalent to d - 9. + if (d > 9) d -= 9 + } + d + }.sum() + // Append the calculated check digit to the 14-digit prefix. + prefix + ((10 - (sum % 10)) % 10) + } + + val computedMeid = meid.computeSpoof { (1..14).map { "0123456789ABCDEF".random() }.joinToString("") }?.uppercase() + val computedImsi = imsi.computeSpoof { generateRandomNumeric(15) } + val computedIccid = iccid.computeSpoof { "89" + generateRandomNumeric(17) } + val computedPhone = phone.computeSpoof { "+" + generateRandomNumeric(11) } + + forEachInstructionAsSequence( + match = { _, _, instruction, instructionIndex -> + if (instruction !is ReferenceInstruction) return@forEachInstructionAsSequence null + + val reference = instruction.reference as? MethodReference ?: return@forEachInstructionAsSequence null + + val match = MethodCall.entries.firstOrNull { search -> + MethodUtil.methodSignaturesMatch(reference, search.reference) + } ?: return@forEachInstructionAsSequence null + + val replacement = when (match) { + MethodCall.NetworkCountryIso -> networkCountryIso?.lowercase() + MethodCall.NetworkOperator -> networkOperator?.toString() + MethodCall.NetworkOperatorName -> networkOperatorName + MethodCall.SimCountryIso -> simCountryIso?.lowercase() + MethodCall.SimOperator -> simOperator?.toString() + MethodCall.SimOperatorName -> simOperatorName + MethodCall.Imei, MethodCall.ImeiWithSlot, MethodCall.DeviceId, MethodCall.DeviceIdWithSlot -> computedImei + MethodCall.Meid, MethodCall.MeidWithSlot -> computedMeid + MethodCall.SubscriberId, MethodCall.SubscriberIdWithSlot -> computedImsi + MethodCall.SimSerialNumber, MethodCall.SimSerialNumberWithSlot -> computedIccid + MethodCall.Line1Number, MethodCall.Line1NumberWithSlot -> computedPhone + } + replacement?.let { instructionIndex to it } + }, + transform = ::transformMethodCall + ) + } + }, ) } -private fun transformMethodCall( - mutableMethod: MutableMethod, - entry: Pair, -) { - val (instructionIndex, methodCallValue) = entry +private fun transformMethodCall(mutableMethod: MutableMethod, entry: Pair) { + val (index, value) = entry + val nextInstr = mutableMethod.getInstruction(index + 1) - // Get the register which would have contained the return value - val register = mutableMethod.getInstruction(instructionIndex + 1).registerA + if (nextInstr.opcode.name != "move-result-object") { + mutableMethod.replaceInstruction(index, "nop") + return + } - // Replace the move-result instruction with our fake value - mutableMethod.replaceInstruction( - instructionIndex + 1, - "const-string v$register, \"$methodCallValue\"", - ) + val register = (nextInstr as OneRegisterInstruction).registerA + mutableMethod.replaceInstruction(index, "const-string v$register, \"$value\"") + mutableMethod.replaceInstruction(index + 1, "nop") } private enum class MethodCall( @@ -154,4 +229,100 @@ private enum class MethodCall( "Ljava/lang/String;", ), ), + Imei( + ImmutableMethodReference( + "Landroid/telephony/TelephonyManager;", + "getImei", + emptyList(), + "Ljava/lang/String;" + ), + ), + ImeiWithSlot( + ImmutableMethodReference( + "Landroid/telephony/TelephonyManager;", + "getImei", + listOf("I"), + "Ljava/lang/String;" + ), + ), + DeviceId( + ImmutableMethodReference( + "Landroid/telephony/TelephonyManager;", + "getDeviceId", + emptyList(), + "Ljava/lang/String;" + ), + ), + DeviceIdWithSlot( + ImmutableMethodReference( + "Landroid/telephony/TelephonyManager;", + "getDeviceId", + listOf("I"), + "Ljava/lang/String;" + ), + ), + Meid( + ImmutableMethodReference( + "Landroid/telephony/TelephonyManager;", + "getMeid", + emptyList(), + "Ljava/lang/String;" + ), + ), + MeidWithSlot( + ImmutableMethodReference( + "Landroid/telephony/TelephonyManager;", + "getMeid", + listOf("I"), + "Ljava/lang/String;" + ), + ), + SubscriberId( + ImmutableMethodReference( + "Landroid/telephony/TelephonyManager;", + "getSubscriberId", + emptyList(), + "Ljava/lang/String;" + ) + ), + SubscriberIdWithSlot( + ImmutableMethodReference( + "Landroid/telephony/TelephonyManager;", + "getSubscriberId", + listOf("I"), + "Ljava/lang/String;" + ) + ), + SimSerialNumber( + ImmutableMethodReference( + "Landroid/telephony/TelephonyManager;", + "getSimSerialNumber", + emptyList(), + "Ljava/lang/String;" + ) + ), + SimSerialNumberWithSlot( + ImmutableMethodReference( + "Landroid/telephony/TelephonyManager;", + "getSimSerialNumber", + listOf("I"), + "Ljava/lang/String;" + ) + ), + Line1Number( + ImmutableMethodReference( + "Landroid/telephony/TelephonyManager;", + "getLine1Number", + emptyList(), + "Ljava/lang/String;" + ) + ), + Line1NumberWithSlot( + ImmutableMethodReference( + "Landroid/telephony/TelephonyManager;", + "getLine1Number", + listOf("I"), + "Ljava/lang/String;" + ) + ) } diff --git a/patches/src/main/kotlin/app/revanced/patches/all/misc/connectivity/wifi/spoof/SpoofWifiPatch.kt b/patches/src/main/kotlin/app/revanced/patches/all/misc/connectivity/wifi/spoof/SpoofWifiPatch.kt index 1666a27a8d..e00f37feaf 100644 --- a/patches/src/main/kotlin/app/revanced/patches/all/misc/connectivity/wifi/spoof/SpoofWifiPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/all/misc/connectivity/wifi/spoof/SpoofWifiPatch.kt @@ -3,7 +3,7 @@ package app.revanced.patches.all.misc.connectivity.wifi.spoof import app.revanced.patcher.patch.bytecodePatch import app.revanced.patches.all.misc.transformation.IMethodCall import app.revanced.patches.all.misc.transformation.filterMapInstruction35c -import app.revanced.patches.all.misc.transformation.transformInstructionsPatch +import app.revanced.util.forEachInstructionAsSequence private const val EXTENSION_CLASS_DESCRIPTOR_PREFIX = "Lapp/revanced/extension/all/misc/connectivity/wifi/spoof/SpoofWifiPatch" @@ -19,29 +19,32 @@ val spoofWiFiConnectionPatch = bytecodePatch( extendWith("extensions/all/misc/connectivity/wifi/spoof/spoof-wifi.rve") dependsOn( - transformInstructionsPatch( - filterMap = { classDef, _, instruction, instructionIndex -> - filterMapInstruction35c( - EXTENSION_CLASS_DESCRIPTOR_PREFIX, - classDef, - instruction, - instructionIndex, - ) - }, - transform = { method, entry -> - val (methodType, instruction, instructionIndex) = entry - methodType.replaceInvokeVirtualWithExtension( - EXTENSION_CLASS_DESCRIPTOR, - method, - instruction, - instructionIndex, - ) - }, - ), + bytecodePatch { + apply { + forEachInstructionAsSequence( + match = { classDef, _, instruction, instructionIndex -> + filterMapInstruction35c( + EXTENSION_CLASS_DESCRIPTOR_PREFIX, + classDef, + instruction, + instructionIndex, + ) + }, + transform = { method, entry -> + val (methodType, instruction, instructionIndex) = entry + methodType.replaceInvokeVirtualWithExtension( + EXTENSION_CLASS_DESCRIPTOR, + method, + instruction, + instructionIndex, + ) + }) + } + }, ) } -// Information about method calls we want to replace +// Information about method calls we want to replace. @Suppress("unused") private enum class MethodCall( override val definedClassName: String, @@ -89,13 +92,13 @@ private enum class MethodCall( "Landroid/net/NetworkInfo;", "getState", arrayOf(), - "Landroid/net/NetworkInfo\$State;", + $$"Landroid/net/NetworkInfo$State;", ), GetDetailedState( "Landroid/net/NetworkInfo;", "getDetailedState", arrayOf(), - "Landroid/net/NetworkInfo\$DetailedState;", + $$"Landroid/net/NetworkInfo$DetailedState;", ), IsActiveNetworkMetered( "Landroid/net/ConnectivityManager;", @@ -132,7 +135,7 @@ private enum class MethodCall( "registerBestMatchingNetworkCallback", arrayOf( "Landroid/net/NetworkRequest;", - "Landroid/net/ConnectivityManager\$NetworkCallback;", + $$"Landroid/net/ConnectivityManager$NetworkCallback;", "Landroid/os/Handler;", ), "V", @@ -140,19 +143,19 @@ private enum class MethodCall( RegisterDefaultNetworkCallback1( "Landroid/net/ConnectivityManager;", "registerDefaultNetworkCallback", - arrayOf("Landroid/net/ConnectivityManager\$NetworkCallback;"), + arrayOf($$"Landroid/net/ConnectivityManager$NetworkCallback;"), "V", ), RegisterDefaultNetworkCallback2( "Landroid/net/ConnectivityManager;", "registerDefaultNetworkCallback", - arrayOf("Landroid/net/ConnectivityManager\$NetworkCallback;", "Landroid/os/Handler;"), + arrayOf($$"Landroid/net/ConnectivityManager$NetworkCallback;", "Landroid/os/Handler;"), "V", ), RegisterNetworkCallback1( "Landroid/net/ConnectivityManager;", "registerNetworkCallback", - arrayOf("Landroid/net/NetworkRequest;", "Landroid/net/ConnectivityManager\$NetworkCallback;"), + arrayOf("Landroid/net/NetworkRequest;", $$"Landroid/net/ConnectivityManager$NetworkCallback;"), "V", ), RegisterNetworkCallback2( @@ -166,7 +169,7 @@ private enum class MethodCall( "registerNetworkCallback", arrayOf( "Landroid/net/NetworkRequest;", - "Landroid/net/ConnectivityManager\$NetworkCallback;", + $$"Landroid/net/ConnectivityManager$NetworkCallback;", "Landroid/os/Handler;", ), "V", @@ -174,13 +177,13 @@ private enum class MethodCall( RequestNetwork1( "Landroid/net/ConnectivityManager;", "requestNetwork", - arrayOf("Landroid/net/NetworkRequest;", "Landroid/net/ConnectivityManager\$NetworkCallback;"), + arrayOf("Landroid/net/NetworkRequest;", $$"Landroid/net/ConnectivityManager$NetworkCallback;"), "V", ), RequestNetwork2( "Landroid/net/ConnectivityManager;", "requestNetwork", - arrayOf("Landroid/net/NetworkRequest;", "Landroid/net/ConnectivityManager\$NetworkCallback;", "I"), + arrayOf("Landroid/net/NetworkRequest;", $$"Landroid/net/ConnectivityManager$NetworkCallback;", "I"), "V", ), RequestNetwork3( @@ -188,7 +191,7 @@ private enum class MethodCall( "requestNetwork", arrayOf( "Landroid/net/NetworkRequest;", - "Landroid/net/ConnectivityManager\$NetworkCallback;", + $$"Landroid/net/ConnectivityManager$NetworkCallback;", "Landroid/os/Handler;", ), "V", @@ -204,7 +207,7 @@ private enum class MethodCall( "requestNetwork", arrayOf( "Landroid/net/NetworkRequest;", - "Landroid/net/ConnectivityManager\$NetworkCallback;", + $$"Landroid/net/ConnectivityManager$NetworkCallback;", "Landroid/os/Handler;", "I", ), @@ -213,7 +216,7 @@ private enum class MethodCall( UnregisterNetworkCallback1( "Landroid/net/ConnectivityManager;", "unregisterNetworkCallback", - arrayOf("Landroid/net/ConnectivityManager\$NetworkCallback;"), + arrayOf($$"Landroid/net/ConnectivityManager$NetworkCallback;"), "V", ), UnregisterNetworkCallback2( diff --git a/patches/src/main/kotlin/app/revanced/patches/all/misc/playintegrity/DisablePlayIntegrity.kt b/patches/src/main/kotlin/app/revanced/patches/all/misc/play/DisablePlayIntegrity.kt similarity index 95% rename from patches/src/main/kotlin/app/revanced/patches/all/misc/playintegrity/DisablePlayIntegrity.kt rename to patches/src/main/kotlin/app/revanced/patches/all/misc/play/DisablePlayIntegrity.kt index 12461fc40a..dd5dad79e4 100644 --- a/patches/src/main/kotlin/app/revanced/patches/all/misc/playintegrity/DisablePlayIntegrity.kt +++ b/patches/src/main/kotlin/app/revanced/patches/all/misc/play/DisablePlayIntegrity.kt @@ -1,4 +1,4 @@ -package app.revanced.patches.all.misc.playintegrity +package app.revanced.patches.all.misc.play import app.revanced.patcher.extensions.replaceInstruction import app.revanced.patcher.patch.bytecodePatch @@ -9,7 +9,7 @@ import com.android.tools.smali.dexlib2.iface.reference.MethodReference import com.android.tools.smali.dexlib2.immutable.reference.ImmutableMethodReference import com.android.tools.smali.dexlib2.util.MethodUtil -private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/playintegrity/DisablePlayIntegrityPatch;" +private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/play/DisablePlayIntegrityPatch;" private val CONTEXT_BIND_SERVICE_METHOD_REFERENCE = ImmutableMethodReference( "Landroid/content/Context;", diff --git a/patches/src/main/kotlin/app/revanced/patches/all/misc/play/SpoofPlayAgeSignals.kt b/patches/src/main/kotlin/app/revanced/patches/all/misc/play/SpoofPlayAgeSignals.kt new file mode 100644 index 0000000000..ccb41b81b4 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/all/misc/play/SpoofPlayAgeSignals.kt @@ -0,0 +1,138 @@ +package app.revanced.patches.all.misc.play + +import app.revanced.patcher.extensions.addInstructions +import app.revanced.patcher.extensions.getInstruction +import app.revanced.patcher.extensions.removeInstructions +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.patch.intOption +import app.revanced.patcher.patch.option +import app.revanced.util.forEachInstructionAsSequence +import app.revanced.util.getReference +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction +import com.android.tools.smali.dexlib2.iface.reference.MethodReference +import com.android.tools.smali.dexlib2.immutable.reference.ImmutableMethodReference + +@Suppress("unused") +val spoofPlayAgeSignalsPatch = bytecodePatch( + name = "Spoof Play Age Signals", + description = "Spoofs Google Play data about the user's age and verification status.", + use = false, +) { + val lowerAgeBound by intOption( + name = "Lower age bound", + description = "A positive integer.", + default = 18, + validator = { it == null || it > 0 }, + ) + + val upperAgeBound by intOption( + name = "Upper age bound", + description = "A positive integer. Must be greater than the lower age bound.", + default = Int.MAX_VALUE, + validator = { it == null || it > lowerAgeBound!! }, + ) + + val userStatus by intOption( + name = "User status", + description = "An integer representing the user status.", + default = UserStatus.VERIFIED.value, + values = UserStatus.entries.associate { it.name to it.value }, + ) + + apply { + forEachInstructionAsSequence(match = { classDef, _, instruction, instructionIndex -> + // Avoid patching the library itself. + if (classDef.type.startsWith("Lcom/google/android/play/agesignals/")) return@forEachInstructionAsSequence null + + // Keep method calls only. + val reference = instruction.getReference() + ?: return@forEachInstructionAsSequence null + + val match = MethodCall.entries.firstOrNull { + reference == it.reference + } ?: return@forEachInstructionAsSequence null + + val replacement = when (match) { + MethodCall.AgeLower -> lowerAgeBound!! + MethodCall.AgeUpper -> upperAgeBound!! + MethodCall.UserStatus -> userStatus!! + } + + replacement.let { instructionIndex to it } + }, transform = { method, entry -> + val (instructionIndex, replacement) = entry + + // Get the register which would have contained the return value. + val register = method.getInstruction(instructionIndex + 1).registerA + + // Replace the call instructions with the spoofed value. + method.removeInstructions(instructionIndex, 2) + method.addInstructions( + instructionIndex, + """ + const v$register, $replacement + invoke-static { v$register }, Ljava/lang/Integer;->valueOf(I)Ljava/lang/Integer; + move-result-object v$register + """.trimIndent(), + ) + }) + } +} + +/** + * See [AgeSignalsResult](https://developer.android.com/google/play/age-signals/reference/com/google/android/play/agesignals/AgeSignalsResult). + */ +private enum class MethodCall( + val reference: MethodReference, +) { + AgeLower( + ImmutableMethodReference( + "Lcom/google/android/play/agesignals/AgeSignalsResult;", + "ageLower", + emptyList(), + "Ljava/lang/Integer;", + ), + ), + AgeUpper( + ImmutableMethodReference( + "Lcom/google/android/play/agesignals/AgeSignalsResult;", + "ageUpper", + emptyList(), + "Ljava/lang/Integer;", + ), + ), + UserStatus( + ImmutableMethodReference( + "Lcom/google/android/play/agesignals/AgeSignalsResult;", + "userStatus", + emptyList(), + "Ljava/lang/Integer;", + ), + ), +} + +/** + * All possible user verification statuses. + * + * See [AgeSignalsVerificationStatus](https://developer.android.com/google/play/age-signals/reference/com/google/android/play/agesignals/model/AgeSignalsVerificationStatus). + */ +private enum class UserStatus(val value: Int) { + /** The user provided their age, but it hasn't been verified yet. */ + DECLARED(5), + + /** The user is 18+. */ + VERIFIED(0), + + /** The user's guardian has set the age for him. */ + SUPERVISED(1), + + /** The user's guardian hasn't approved the significant changes yet. */ + SUPERVISED_APPROVAL_PENDING(2), + + /** The user's guardian has denied approval for one or more pending significant changes. */ + SUPERVISED_APPROVAL_DENIED(3), + + /** The user is not verified or supervised. */ + UNKNOWN(4), +} diff --git a/patches/src/main/kotlin/app/revanced/patches/all/misc/spoof/SpoofKeystoreSecurityLevelPatch.kt b/patches/src/main/kotlin/app/revanced/patches/all/misc/spoof/SpoofKeystoreSecurityLevelPatch.kt new file mode 100644 index 0000000000..b45cfb4e0d --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/all/misc/spoof/SpoofKeystoreSecurityLevelPatch.kt @@ -0,0 +1,28 @@ +package app.revanced.patches.all.misc.spoof + +import app.revanced.patcher.extensions.replaceInstructions +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.util.forEachInstructionAsSequence + +@Suppress("unused") +val spoofKeystoreSecurityLevelPatch = bytecodePatch( + name = "Spoof keystore security level", + description = "Forces apps to see Keymaster and Attestation security levels as 'StrongBox' (Level 2).", + use = false +) { + apply { + forEachInstructionAsSequence( + match = { _, method, _, _ -> + // Match methods by comparing the current method to a reference criteria. + val name = method.name.lowercase() + if (name.contains("securitylevel") && method.returnType == "I") method else null + }, + transform = { mutableMethod, _ -> + // Ensure the method has an implementation before replacing. + if (mutableMethod.implementation?.instructions?.iterator()?.hasNext() == true) { + mutableMethod.replaceInstructions(0, "const/4 v0, 0x2\nreturn v0") + } + } + ) + } +} \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/all/misc/spoof/SpoofRootOfTrustPatch.kt b/patches/src/main/kotlin/app/revanced/patches/all/misc/spoof/SpoofRootOfTrustPatch.kt new file mode 100644 index 0000000000..6177b7e317 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/all/misc/spoof/SpoofRootOfTrustPatch.kt @@ -0,0 +1,70 @@ +package app.revanced.patches.all.misc.spoof + +import app.revanced.patcher.extensions.replaceInstructions +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.util.forEachInstructionAsSequence +import com.android.tools.smali.dexlib2.iface.reference.MethodReference +import com.android.tools.smali.dexlib2.immutable.reference.ImmutableMethodReference +import com.android.tools.smali.dexlib2.util.MethodUtil + +@Suppress("unused") +val spoofRootOfTrustPatch = bytecodePatch( + name = "Spoof root of trust", + description = "Spoofs device integrity states (Locked Bootloader, Verified OS) for apps that perform local certificate attestation.", + use = false +) { + apply { + forEachInstructionAsSequence( + match = { _, method, _, _ -> + MethodCall.entries.firstOrNull { MethodUtil.methodSignaturesMatch(method, it.reference) } + }, + transform = { mutableMethod, methodCall -> + if (mutableMethod.implementation?.instructions?.iterator()?.hasNext() == true) { + mutableMethod.replaceInstructions(0, methodCall.replacementInstructions) + } + } + ) + } +} + +private enum class MethodCall( + val reference: MethodReference, + val replacementInstructions: String, +) { + IsDeviceLockedRootOfTrust( + ImmutableMethodReference( + "LRootOfTrust;", + "isDeviceLocked", + emptyList(), + "Z" + ), + "const/4 v0, 0x1\nreturn v0", + ), + GetVerifiedBootStateRootOfTrust( + ImmutableMethodReference( + "LRootOfTrust;", + "getVerifiedBootState", + emptyList(), + "I" + ), + "const/4 v0, 0x0\nreturn v0", + ), + IsDeviceLockedAttestation( + ImmutableMethodReference( + "LAttestation;", + "isDeviceLocked", + emptyList(), + "Z" + ), + "const/4 v0, 0x1\nreturn v0", + ), + GetVerifiedBootStateAttestation( + ImmutableMethodReference( + "LAttestation;", + "getVerifiedBootState", + emptyList(), + "I" + ), + "const/4 v0, 0x0\nreturn v0", + ), +} \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/instagram/story/locationsticker/EnableLocationStickerRedesignPatch.kt b/patches/src/main/kotlin/app/revanced/patches/instagram/story/locationsticker/EnableLocationStickerRedesignPatch.kt new file mode 100644 index 0000000000..443d60d155 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/instagram/story/locationsticker/EnableLocationStickerRedesignPatch.kt @@ -0,0 +1,20 @@ +package app.revanced.patches.instagram.story.locationsticker + +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.util.returnEarly + +@Suppress("unused") +val enableLocationStickerRedesignPatch = bytecodePatch( + name = "Enable location sticker redesign", + description = "Unlocks the redesigned location sticker with additional style options.", + use = false, +) { + compatibleWith("com.instagram.android") + + apply { + // The gate method reads a MobileConfig boolean flag and returns it directly. + // Returning early with true bypasses the flag check entirely, + // enabling the redesigned sticker styles regardless of server configuration. + locationStickerRedesignGateMethodMatch.method.returnEarly(true) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/instagram/story/locationsticker/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/instagram/story/locationsticker/Fingerprints.kt new file mode 100644 index 0000000000..0972d692c9 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/instagram/story/locationsticker/Fingerprints.kt @@ -0,0 +1,16 @@ +package app.revanced.patches.instagram.story.locationsticker + +import app.revanced.patcher.composingFirstMethod +import app.revanced.patcher.instructions +import app.revanced.patcher.invoke +import app.revanced.patcher.patch.BytecodePatchContext + +// MobileConfig boolean key that gates the redesigned location sticker styles. +// The method containing this constant reads the flag and returns it directly, +// making it the sole control point for the feature. The key is stable across +// app updates as MobileConfig keys are server-assigned constants. +private const val LOCATION_STICKER_REDESIGN_CONFIG_KEY = 0x8105a100041e0dL + +internal val BytecodePatchContext.locationStickerRedesignGateMethodMatch by composingFirstMethod { + instructions(LOCATION_STICKER_REDESIGN_CONFIG_KEY()) +} diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/layout/branding/BaseCustomBrandingPatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/layout/branding/BaseCustomBrandingPatch.kt index 35baad31cf..9a583e2c7a 100644 --- a/patches/src/main/kotlin/app/revanced/patches/shared/layout/branding/BaseCustomBrandingPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/shared/layout/branding/BaseCustomBrandingPatch.kt @@ -3,7 +3,6 @@ package app.revanced.patches.shared.layout.branding import app.revanced.com.android.tools.smali.dexlib2.mutable.MutableMethod import app.revanced.patcher.extensions.addInstruction import app.revanced.patcher.extensions.getInstruction -import app.revanced.patcher.firstImmutableClassDef import app.revanced.patcher.patch.* import app.revanced.patches.all.misc.packagename.setOrGetFallbackPackageName import app.revanced.patches.all.misc.resources.addResources @@ -126,14 +125,14 @@ internal fun baseCustomBrandingPatch( val getBuilderIndex = if (isYouTubeMusic) { // YT Music the field is not a plain object type. indexOfFirstInstructionOrThrow { - getReference()?.type == "Landroid/app/Notification\$Builder;" + getReference()?.type == $$"Landroid/app/Notification$Builder;" } } else { // Find the field name of the notification builder. Field is an Object type. val builderCastIndex = indexOfFirstInstructionOrThrow { val reference = getReference() opcode == Opcode.CHECK_CAST && - reference?.type == "Landroid/app/Notification\$Builder;" + reference?.type == $$"Landroid/app/Notification$Builder;" } indexOfFirstInstructionReversedOrThrow(builderCastIndex) { getReference()?.type == "Ljava/lang/Object;" @@ -148,11 +147,11 @@ internal fun baseCustomBrandingPatch( ).forEach { index -> addInstructionsAtControlFlowLabel( index, - """ + $$""" move-object/from16 v0, p0 - iget-object v0, v0, $builderFieldName - check-cast v0, Landroid/app/Notification${'$'}Builder; - invoke-static { v0 }, $EXTENSION_CLASS_DESCRIPTOR->setNotificationIcon(Landroid/app/Notification${'$'}Builder;)V + iget-object v0, v0, $$builderFieldName + check-cast v0, Landroid/app/Notification$Builder; + invoke-static { v0 }, $$EXTENSION_CLASS_DESCRIPTOR->setNotificationIcon(Landroid/app/Notification$Builder;)V """, ) } @@ -162,16 +161,37 @@ internal fun baseCustomBrandingPatch( ) afterDependents { + val useCustomName = customName != null + val useCustomIcon = customIcon != null + val isRootInstall = setOrGetFallbackPackageName(originalAppPackageName) == originalAppPackageName + // Can only check if app is root installation by checking if change package name patch is in use. // and can only do that in the afterDependents block here. // The UI preferences cannot be selectively added here, because the settings afterDependents block // may have already run and the settings are already wrote to file. // Instead, show a warning if any patch option was used (A rooted device launcher ignores the manifest changes), // and the non-functional in-app settings are removed on app startup by extension code. - if (customName != null || customIcon != null) { - if (setOrGetFallbackPackageName(originalAppPackageName) == originalAppPackageName) { - Logger.getLogger(this::class.java.name).warning( - "Custom branding does not work with root installation. No changes applied.", + if (isRootInstall && (useCustomName || useCustomIcon)) { + Logger.getLogger(this::class.java.name).warning( + "Custom branding does not work with root installation. No changes applied." + ) + } + + if (!isRootInstall || useCustomName) { + document("AndroidManifest.xml").use { document -> + val application = document.getElementsByTagName("application").item(0) as Element + application.setAttribute( + "android:label", + if (useCustomName) { + // Use custom name everywhere. + customName + } else { + // The YT application name can appear in some places alongside the system + // YouTube app, such as the settings app list and in the "open with" file picker. + // Because the YouTube app cannot be completely uninstalled and only disabled, + // use a custom name for this situation to disambiguate which app is which. + "@string/revanced_custom_branding_name_entry_2" + } ) } } @@ -312,16 +332,19 @@ internal fun baseCustomBrandingPatch( activityAliasNameWithIntents, ).childNodes - // The YT application name can appear in some places alongside the system - // YouTube app, such as the settings app list and in the "open with" file picker. - // Because the YouTube app cannot be completely uninstalled and only disabled, - // use a custom name for this situation to disambiguate which app is which. - application.setAttribute( - "android:label", - "@string/revanced_custom_branding_name_entry_2", - ) + // If user provides a custom icon, then change the application icon ('static' icon) + // which shows as the push notification for some devices, in the app settings, + // and as the icon for the apk before installing. + // This icon cannot be dynamically selected and this change must only be done if the + // user provides an icon otherwise there is no way to restore the original YouTube icon. + if (useCustomIcon) { + application.setAttribute( + "android:icon", + "@mipmap/revanced_launcher_custom" + ) + } - val enabledNameIndex = if (useCustomName) numberOfPresetAppNames else 1 // 1 indexing. + val enabledNameIndex = if (useCustomName) numberOfPresetAppNames else 2 // 1 indexing. val enabledIconIndex = if (useCustomIcon) iconStyleNames.size else 0 // 0 indexing. for (appNameIndex in 1..numberOfPresetAppNames) { @@ -336,7 +359,7 @@ internal fun baseCustomBrandingPatch( iconMipmapName = originalLauncherIconName, appNameIndex = appNameIndex, useCustomName = useCustomNameLabel, - enabled = (appNameIndex == 1), + enabled = false, intentFilters, ), ) diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/misc/spoof/SpoofVideoStreamsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/misc/spoof/SpoofVideoStreamsPatch.kt index 7662718fc8..1c918b8104 100644 --- a/patches/src/main/kotlin/app/revanced/patches/shared/misc/spoof/SpoofVideoStreamsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/shared/misc/spoof/SpoofVideoStreamsPatch.kt @@ -169,13 +169,13 @@ internal fun spoofVideoStreamsPatch( if-eqz v2, :disabled # Get streaming data. - invoke-static { v2 }, $EXTENSION_CLASS_DESCRIPTOR->getStreamingData(Ljava/lang/String;)Ljava/nio/ByteBuffer; + invoke-static { v2 }, $EXTENSION_CLASS_DESCRIPTOR->getStreamingData(Ljava/lang/String;)[B move-result-object v3 if-eqz v3, :disabled # Parse streaming data. sget-object v4, $playerProtoClass->a:$playerProtoClass - invoke-static { v4, v3 }, $protobufClass->parseFrom(${protobufClass}Ljava/nio/ByteBuffer;)$protobufClass + invoke-static { v4, v3 }, $protobufClass->parseFrom(${protobufClass}[B)$protobufClass move-result-object v5 check-cast v5, $playerProtoClass diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/spoof/SpoofVideoStreamsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/spoof/SpoofVideoStreamsPatch.kt index 980e64888e..05a8f57caf 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/spoof/SpoofVideoStreamsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/spoof/SpoofVideoStreamsPatch.kt @@ -63,10 +63,10 @@ val spoofVideoStreamsPatch = spoofVideoStreamsPatch( // Requires a key and title but the actual text is chosen at runtime. key = "revanced_spoof_video_streams_about", summaryKey = null, - tag = "app.revanced.extension.youtube.settings.preference.SpoofStreamingDataSideEffectsPreference", + tag = "app.revanced.extension.youtube.settings.preference.SpoofVideoStreamsSideEffectsPreference", ), SwitchPreference("revanced_spoof_video_streams_av1"), - SwitchPreference("revanced_spoof_streaming_data_stats_for_nerds"), + SwitchPreference("revanced_spoof_video_streams_stats_for_nerds"), ), ), ) diff --git a/patches/src/main/resources/addresources/values-cs-rCZ/strings.xml b/patches/src/main/resources/addresources/values-cs-rCZ/strings.xml index a4105694a5..d1ef713d1b 100644 --- a/patches/src/main/resources/addresources/values-cs-rCZ/strings.xml +++ b/patches/src/main/resources/addresources/values-cs-rCZ/strings.xml @@ -25,6 +25,7 @@ Second \"item\" text" Vlastní Ikona aplikace Původní + ReVanced ReVanced minimální ReVanced škálované @@ -49,6 +50,7 @@ Second \"item\" text" Nastavení + ReVanced Opravdu chcete pokračovat? Uložit Výchozí @@ -131,6 +133,8 @@ Klepněte na tlačítko Pokračovat a povolte změny optimalizace." Falšovat video streamy Falšovat video streamy klienta, aby se předešlo problémům s přehráváním. + Falšovat video streamy + Falšovat video streamy klienta, aby se předešlo problémům s přehráváním Napodobovat video streamy "Video streamy jsou maskovány @@ -218,9 +222,12 @@ Povolením této možnosti se však budou zaznamenávat i některá uživatelsk Přísun Obecné Přehrávač + Shorts Lišta Ovládání gesty + Return YouTube Dislike Různé + Video Obnovit staré menu nastavení Staré menu nastavení se zobrazují Staré menu nastavení se nezobrazují @@ -1148,6 +1155,7 @@ Nastavení → Přehrávání → Automatické přehrávání dalšího videa"Načtěte video znovu, abyste hlasovali pomocí Return YouTube Dislike Skryto vlastníkem + Return YouTube Dislike Nelíbí se se zobrazují Nelíbí se se nezobrazují Zobrazit nelíbí se v Shorts @@ -1168,6 +1176,7 @@ Omezení: Počty „Nelíbí se mi“ se nemusí zobrazit v anonymním režimu"< Zobrazit toast, pokud API není dostupné Toast se zobrazí, pokud Return YouTube Dislike není dostupný Toast se nezobrazí, pokud Return YouTube Dislike není dostupný + ReturnYouTubeDislike.com Data jsou poskytována API Return YouTube Dislike. Klepnutím se dozvíte více Statistiky API ReturnYouTubeDislike tohoto zařízení @@ -1188,6 +1197,7 @@ Omezení: Počty „Nelíbí se mi“ se nemusí zobrazit v anonymním režimu"< %d milisekund + SponsorBlock Povolit SponsorBlock SponsorBlock je systém s participací komunity pro přeskakování otravných částí videí na YouTube Vzhled @@ -1396,12 +1406,14 @@ Jste připraveni k odeslání?" Průhlednost: Barva: O aplikaci + sponsor.ajay.app Data poskytuje rozhraní API SponsorBlock. Klepněte zde, abyste se dozvěděli více a zobrazili si soubory ke stažení pro další platformy Rozvržení formuláře Výchozí Telefon + Tablet Automobilový "Změny zahrnují: @@ -1445,6 +1457,7 @@ Pokud bude později vypnuta, doporučujeme vymazat data aplikace, aby se zabrán Playlisty Hledat Nakupování + Shorts Sport Odběry Trendy @@ -1483,6 +1496,7 @@ Omezení: Použití tlačítka zpět na panelu nástrojů nemusí fungovat"Vypnuto Výchozí Minimální + Tablet Moderní 1 Moderní 2 Moderní 3 @@ -1509,6 +1523,11 @@ Omezení: Použití tlačítka zpět na panelu nástrojů nemusí fungovat"Skrýt tlačítka překrytí Tlačítka překrytí jsou skrytá Tlačítka překrytí jsou zobrazena + Skrýt tlačítka rozbalit a zavřít + "Tlačítka jsou skrytá + +Přejetím roztáhnete nebo zavřete" + Tlačítka rozbalit a zavřít jsou zobrazena Skrýt podtexty Podtexty jsou skryty Podtexty jsou zobrazeny @@ -1539,11 +1558,17 @@ Omezení: Použití tlačítka zpět na panelu nástrojů nemusí fungovat"Zvýrazněná barva posuvníku Neplatná hodnota barvy posuvníku - + + YouTube ReVanced + YT ReVanced + YT + Logo záhlaví Výchozí Běžné + Prémium + ReVanced ReVanced minimální Vlastní @@ -1568,6 +1593,7 @@ Povolení této funkce může opravit chybějící obrázky, které jsou v někt DeArrow & Původní náhledy DeArrow & Zachycení snímků Zachycení snímků + DeArrow "DeArrow poskytuje miniatury videí YouTube z crowdsourcingu. Tyto miniatury jsou často relevantnější než ty, které poskytuje YouTube Pokud je tato funkce povolena, budou adresy URL videí odeslány na server API a nebudou odesílány žádné další údaje. Pokud video nemá miniatury DeArrow, zobrazí se originální nebo statické snímky @@ -1611,7 +1637,11 @@ Klepnutím sem se dozvíte více o DeArrow" Smyčka videa je zapnuta Smyčka videa je vypnuta - + + Pozastavit při přerušení zvuku + Přehrávání se pozastaví, když se přehrává jiný zvuk (např. navigace) + Sníží hlasitost při přehrávání ostatních zvuků + Napodobovat rozměry zařízení "Rozměry zařízení jsou zfalšovány @@ -1766,7 +1796,11 @@ Přehrávání videa s AV1 se může sekat nebo vypadávat snímky." - + + YT Music ReVanced + Music ReVanced + Hudba + O aplikaci @@ -1893,6 +1927,7 @@ Přehrávání videa s AV1 se může sekat nebo vypadávat snímky." O ReVanced Blokování reklam Nastavení blokování reklam + Chat Nastavení chatu Různé Různé nastavení diff --git a/patches/src/main/resources/addresources/values-de-rDE/strings.xml b/patches/src/main/resources/addresources/values-de-rDE/strings.xml index a7d6425153..abd7622a51 100644 --- a/patches/src/main/resources/addresources/values-de-rDE/strings.xml +++ b/patches/src/main/resources/addresources/values-de-rDE/strings.xml @@ -35,14 +35,14 @@ Second \"item\" text" Ignorieren <h5>Diese App wurde offenbar nicht von Ihnen gepatcht.</h5><br>Diese App funktioniert möglicherweise nicht richtig, <b>könnte schädlich oder sogar gefährlich in der Verwendung sein</b>.< br><br>Diese Prüfungen deuten darauf hin, dass diese App vorab gepatcht wurde oder von jemandem bezogen wurde:<br><br><small>%1$s</small><br>Es wird dringend empfohlen, <b>diese App zu deinstallieren und selbst zu patchen</b> um sicherzustellen, dass Sie eine validierte und sichere App verwenden.<p><br>Wenn Sie diese Warnung ignorieren, wird sie nur zweimal angezeigt. Auf einem anderen Gerät gepatcht - Nicht durch ReVanced Manager installiert + Nicht von ReVanced Manager installiert Vor mehr als 10 Minuten gepatcht Vor %s Tagen gepatcht APK Erstellungsdatum ist beschädigt ReVanced Hinweis - Ihr Verlauf wird nicht gespeichert.<br><br>Dies wird höchstwahrscheinlich durch einen DNS-Werbeblocker oder einen Netzwerkproxy verursacht.<br><br>Um dies zu beheben, setze <b>s.youtube.com</b> auf die Whitelist oder schalten Sie alle DNS-Blocker und Proxies aus. + Ihr Verlauf wird nicht gespeichert.<br><br>Dies wird höchstwahrscheinlich durch einen DNS-Werbeblocker oder einen Netzwerkproxy verursacht.<br><br>Um dies zu beheben, setze <b>s.youtube.com</b> auf die Whitelist oder schalten Sie alle DNS-Blocker und Proxys aus. Nicht wieder anzeigen @@ -1598,7 +1598,9 @@ Tippen Sie hier, um mehr über DeArrow zu erfahren" Loop-Video ist aktiviert Loop-Video ist deaktiviert - + + Pause bei Audiounterbrechung + Spoof-Gerätegröße "Gerätemessungen gefälscht @@ -1753,7 +1755,9 @@ Die Videowiedergabe mit AV1 kann stottern oder Bilder überspringen." - + + Musik + Über diff --git a/patches/src/main/resources/addresources/values-el-rGR/strings.xml b/patches/src/main/resources/addresources/values-el-rGR/strings.xml index f8dcb2d550..9322f6183e 100644 --- a/patches/src/main/resources/addresources/values-el-rGR/strings.xml +++ b/patches/src/main/resources/addresources/values-el-rGR/strings.xml @@ -238,13 +238,9 @@ Second \"item\" text" Η αναπαραγωγή παρασκηνίου είναι ενεργοποιημένη για τα Shorts - Ενότητα καταστήματος δημιουργού - Κρυμμένη - -Αφορά την ενότητα καταστήματος δημιουργού κάτω από την οθόνη αναπαραγωγής - Εμφανίζεται - -Αφορά την ενότητα καταστήματος δημιουργού κάτω από την οθόνη αναπαραγωγής + Ενότητα καταστήματος δημιουργού κάτω από την οθόνη αναπαραγωγής + Κρυμμένη + Εμφανίζεται Κάρτες άλμπουμ Κρυμμένες Εμφανίζονται @@ -373,13 +369,9 @@ Second \"item\" text" Συγχρονισμένες αντιδράσεις Κρυμμένες Εμφανίζονται - Τίτλος του βίντεο - Κρυμμένος - -Αφορά τον τίτλο του βίντεο στη λειτουργία πλήρους οθόνης - Εμφανίζεται - -Αφορά τον τίτλο του βίντεο στη λειτουργία πλήρους οθόνης + Τίτλος του βίντεο στην πλήρη οθόνη + Κρυμμένος + Εμφανίζεται Σύνοψη βίντεο που δημιουργήθηκε από AI Κρυμμένη Εμφανίζεται @@ -395,13 +387,9 @@ Second \"item\" text" Ενότητα «Πρόοδος μαθήματος» Κρυμμένη Εμφανίζεται - Ενότητες «Εξερεύνηση» - Κρυμμένες - -Αφορά τις ενότητες «Εξερευνήστε αυτή τη σειρά μαθημάτων» και «Εξερευνήστε το podcast» - Εμφανίζονται - -Αφορά τις ενότητες «Εξερευνήστε αυτή τη σειρά μαθημάτων» και «Εξερευνήστε το podcast» + Ενότητες «Εξερευνήστε...» + Κρυμμένες. Αφορά τις ενότητες «Εξερευνήστε αυτή τη σειρά μαθημάτων» και «Εξερευνήστε το podcast» + Εμφανίζονται. Αφορά τις ενότητες «Εξερευνήστε αυτή τη σειρά μαθημάτων» και «Εξερευνήστε το podcast» Ενότητα «Εξερευνήστε αυτή τη σειρά μαθημάτων» Κρυμμένη Εμφανίζεται @@ -1631,8 +1619,8 @@ Second \"item\" text" Εμφάνιση ανακοινώσεων ReVanced - Οι ανακοινώσεις κατά την εκκίνηση εμφανίζονται - Οι ανακοινώσεις κατά την εκκίνηση δεν εμφανίζονται + Οι ανακοινώσεις εμφανίζονται κατά την εκκίνηση + Οι ανακοινώσεις δεν εμφανίζονται κατά την εκκίνηση Εμφάνιση ανακοινώσεων κατά την εκκίνηση Αποτυχία σύνδεσης με τον πάροχο ανακοινώσεων Παράλειψη diff --git a/patches/src/main/resources/addresources/values-es-rES/strings.xml b/patches/src/main/resources/addresources/values-es-rES/strings.xml index a8079d9ff1..c1a1e1d536 100644 --- a/patches/src/main/resources/addresources/values-es-rES/strings.xml +++ b/patches/src/main/resources/addresources/values-es-rES/strings.xml @@ -1514,6 +1514,11 @@ El minireproductor se puede arrastrar fuera de la pantalla hacia la izquierda o Ocultar botones de superposición Los botones de superposición están ocultos Se muestran los botones de superposición + Ocultar botones de expandir y cerrar + "Los botones están ocultos + +Deslizar para expandir o cerrar" + Se muestran botones de expandir y cerrar Ocultar subtextos Los subtextos están ocultos Los subtextos se muestran @@ -1622,7 +1627,10 @@ Toca aquí para obtener más información sobre DeArrow" Bucle de vídeo activado Bucle de vídeo desactivado - + + Pausar cuando se corte el audio + La reproducción se detiene cuando se reproduce otro audio (por ejemplo, navegación) + Dimensiones del dispositivo "Las dimensiones del dispositivo están falsificadas diff --git a/patches/src/main/resources/addresources/values-ga-rIE/strings.xml b/patches/src/main/resources/addresources/values-ga-rIE/strings.xml index 569faafd9d..9a78cc4c2d 100644 --- a/patches/src/main/resources/addresources/values-ga-rIE/strings.xml +++ b/patches/src/main/resources/addresources/values-ga-rIE/strings.xml @@ -370,39 +370,39 @@ Mar sin féin, má chumasaíonn tú é seo, logálfar roinnt sonraí úsáideora Folaigh teideal físeáin Tá teideal an fhíseáin i bhfolach san fhorleagan seinnteora Taispeántar teideal an fhíseáin i bhforleagan an imreora - Folaigh \'Achoimre físeáin arna giniúint ag AI\' + Folaigh \'Achoimre físe a ghintear le hintleacht shaorga\' Tá an chuid achoimre físe IS-ghinte i bhfolach - Taispeántar an chuid achoimre físe a ghintear ag AI + Taispeántar an chuid achoimre físe a ghintear ag Intleacht Shaorga Folaigh Iarr Tá an rannán Iarratas i bhfolach Taispeántar an rannán Iarratas Folaigh Tréithe - Tá ailt d\'áiteanna sonracha, Cluichí, Ceol agus Daoine a luaitear i bhfolach + Tá na rannóga \'Áiteanna Réadmhaoine\', \'Cluichí\', \'Ceol\' agus \'Daoine\' i bhfolach Taispeántar ailt d\'áiteanna sonracha, Cluichí, Ceol agus Daoine a luaitear Folaigh Caibidlí Tá an chuid Caibidil i bhfolach Taispeántar alt na gcaibidlí - Folaigh ‘Dul chun cinn cúrsa’ + Folaigh \'Dul chun cinn an chúrsa\' Tá rannóg an dul chun cinn cúrsa i bhfolach Taispeántar rannóg an dul chun cinn cúrsa Folaigh Iniúchadh Tá rannóga Iniúchadh ar an gcúrsa seo agus Déan iniúchadh ar an bpodchraoladh i bhfolach - Taispeántar rannóga Iniúchadh ar an gcúrsa seo agus Déan iniúchadh ar an bpodchraoladh + Taispeántar na rannóga Iniúchadh an chúrsa seo agus Iniúchadh na podchraoltaí Folaigh ‘Déan iniúchadh ar an gcúrsa seo’ Tá rannóg Déan iniúchadh ar an gcúrsa seo i bhfolach - Taispeántar rannóg Déan iniúchadh ar an gcúrsa seo + Taispeántar an chuid seo den chúrsa a iniúchadh Folaigh \'Déan iniúchadh ar an bpodchraoladh\' Tá an chuid Déan iniúchadh ar an bpodchraoladh i bhfolach - Taispeántar an chuid Déan iniúchadh ar an bpodchraoladh - Folaigh \'Déan iniúchadh ar an bpodchraoladh\' - Tá an chuid Déan iniúchadh ar an bpodchraoladh i bhfolach - Taispeántar an chuid Déan iniúchadh ar an bpodchraoladh + Taispeántar an rannóg podchraoltaí a iniúchadh + Folaigh \'Iniúchadh a dhéanamh ar an bpodchraoladh\' + Tá an rannán podchraoltaí a iniúchadh i bhfolach + Taispeántar an rannóg podchraoltaí a iniúchadh Folaigh naisc le feiceáil Tá an chuid nasc le feiceáil i bhfolach Taispeántar an chuid nasc le feiceáil Folaigh ‘Áiteanna faoi Thrácht’ Tá rannóg na n-áiteanna faoi Thrácht i bhfolach - Taispeántar rannóg na n-áiteanna faoi Thrácht + Taispeántar an chuid áiteanna feiceálacha Folaigh físeáin le feiceáil Tá an chuid físeán le feiceáil i bhfolach Taispeántar an chuid físeán le feiceáil @@ -1847,7 +1847,7 @@ D’fhéadfadh sé go mbeadh stad nó go gcaillfí frámaí ag athsheinm físe l Folaigh nó athraigh cnaipí an bharra nascleanúna Folaigh Baile - Tá cnaipe Baile folaithe + Tá an cnaipe baile i bhfolach Taispeántar an cnaipe baile Folaigh Samplaí @@ -1875,7 +1875,7 @@ D’fhéadfadh sé go mbeadh stad nó go gcaillfí frámaí ag athsheinm físe l Folaigh an lipéad \'Faigh Music Premium\' Tá an lipéad i bhfolach - Taispeántar an lipéad + Taispeántar lipéad Folaigh an cnaipe uasghrádaithe @@ -1885,28 +1885,28 @@ D’fhéadfadh sé go mbeadh stad nó go gcaillfí frámaí ag athsheinm físe l - Cuir bac ar fógraí fuaime - Cuirtear bac ar fhógraí fuaime + Blocáil fógraí fuaime + Tá fógraí fuaime blocáilte Tá fógraí fuaime díbhlocáilte %s neamh-infheidhme, d\'fhéadfadh go dtaispeánfadh fógraí. Bain triail as seirbhís blocála fógraí a athrú sna socruithe. - Tháinig earráid ar %s, d\'fhéadfadh go dtaispeánfadh fógraí. Bain triail as seirbhís blocála fógraí a athrú sna socruithe. + Thug %s earráid ar ais, d’fhéadfadh fógraí a bheith le feiceáil. Bain triail as an tseirbhís blocála fógraí a athrú sna socruithe. Bloc ar fhógraí físe leabaithe Díchumasaithe - Proxy lonrúil + Seachfhreastalaí lonrúil Seachfhreastalaí PurpleAdBlock Bloc ar fhógraí físe - Cuirtear bac ar fhógraí físe - Déantar fógraí físe a dhíbhlocáil + Tá bac ar fhógraí físe + Tá fógraí físe díbhlocáilte Teachtaireacht scriosta Taispeáin teachtaireachtaí scriosta Ná taispeáin teachtaireachtaí scriosta - Folaigh teachtaireachtaí scriosta taobh thiar a fhalsúa + Folaigh teachtaireachtaí scriosta taobh thiar a mhilleann Taispeáin teachtaireachtaí scriosta mar théacs trasnaithe amach diff --git a/patches/src/main/resources/addresources/values-hr-rHR/strings.xml b/patches/src/main/resources/addresources/values-hr-rHR/strings.xml index 69b5287bdb..4134bb2469 100644 --- a/patches/src/main/resources/addresources/values-hr-rHR/strings.xml +++ b/patches/src/main/resources/addresources/values-hr-rHR/strings.xml @@ -29,7 +29,7 @@ Second \"item\" text" - Sačuvaj + Spremi Onemogući podebljane ikone Ikone nisu podebljane Ikone su podebljane @@ -41,7 +41,7 @@ Second \"item\" text" GmsCore Postavke povezane s GmsCoreom Provjeri ažuriranja za GmsCore - Provjera ažuriranja omogućena je + Provjera ažuriranja je omogućena Provjera ažuriranja je onemogućena Otvori postavke za GmsCore Postavke za GmsCore @@ -49,7 +49,7 @@ Second \"item\" text" MicroG GmsCore nije instaliran. Instalirajte ga. Potrebna je radnja Nije uspjela provjera ažuriranja za MicroG GmsCore - Dostupna je nova verzija (%1$s) MicroG GmsCore. Trenutno koristite verziju %2$s. + Dostupna je nova verzija (%1$s) MicroG GmsCorea. Trenutno koristite verziju %2$s. "MicroG GmsCore nema dopuštenje za rad u pozadini.\n\nSlijedite vodič \"Ne ubijaj moju aplikaciju\" za svoj telefon i primijenite upute na svoju MicroG instalaciju.\n\nOvo je potrebno da bi aplikacija radila." Otvori web-stranicu Odustani @@ -438,8 +438,8 @@ Već postoji" Sakrij opcije premium kvalitete - Opcije premium kvalitete su sakrivene - Opcije premium kvalitete su prikazane + Premium opcije kvalitete su skrivene + Premium opcije kvalitete su prikazane diff --git a/patches/src/main/resources/addresources/values-in-rID/strings.xml b/patches/src/main/resources/addresources/values-in-rID/strings.xml index a0182ee1ba..22fad62fe3 100644 --- a/patches/src/main/resources/addresources/values-in-rID/strings.xml +++ b/patches/src/main/resources/addresources/values-in-rID/strings.xml @@ -188,7 +188,7 @@ Anda tidak akan diberi tahu tentang kejadian yang tidak terduga." Catatan protokol buffer Pencatatan debug termasuk buffer proto Pencatatan debug tidak menyertakan buffer proto - "Mengaktifkan setelan ini akan mencatat data tata letak tambahan, termasuk teks pada layar untuk beberapa komponen UI. + "Mengaktifkan pengaturan ini akan mencatat data tata letak tambahan, termasuk teks pada layar untuk beberapa komponen UI. Ini dapat membantu mengidentifikasi komponen saat membuat penyaring khusus. diff --git a/patches/src/main/resources/addresources/values-ja-rJP/strings.xml b/patches/src/main/resources/addresources/values-ja-rJP/strings.xml index 2f418fa95f..3709e8d3a4 100644 --- a/patches/src/main/resources/addresources/values-ja-rJP/strings.xml +++ b/patches/src/main/resources/addresources/values-ja-rJP/strings.xml @@ -1693,13 +1693,13 @@ Automotive レイアウト デフォルトの画質が変更された場合にトースト通知が表示されます デフォルトの画質が変更された場合にトースト通知は表示されません デフォルトの画質 (Wi-Fi) - デフォルトの画質 (携帯回線) + デフォルトの画質 (モバイル) ショートの画質の変更を保存 画質の変更はすべてのショート動画に適用されます 画質の変更は現在のショート動画にのみ適用されます デフォルトのショートの画質 (Wi-Fi) - デフォルトのショートの画質 (携帯回線) - 携帯回線 + デフォルトのショートの画質 (モバイル) + モバイル Wi-Fi デフォルトの画質の変更 (%1$s): %2$s ショートの画質の変更 (%1$s): %2$s diff --git a/patches/src/main/resources/addresources/values-ko-rKR/strings.xml b/patches/src/main/resources/addresources/values-ko-rKR/strings.xml index 886f8e9a8d..b79fccb0ea 100644 --- a/patches/src/main/resources/addresources/values-ko-rKR/strings.xml +++ b/patches/src/main/resources/addresources/values-ko-rKR/strings.xml @@ -649,7 +649,7 @@ YouTube Premium 사용자라면 이 설정은 필요하지 않을 수 있습니 앱 패키지명을 입력하세요 기타 앱이 설치되지 않습니다 - %s 는 설치되어 있지 않습니다. 설치하세요 + %s 는 설치되어 있지 않습니다. 설치하세요. "패키지 이름이 '%s'인 설치된 앱을 찾을 수 없습니다 패키지 이름이 올바르고 앱이 설치되어 있는지 확인하세요" diff --git a/patches/src/main/resources/addresources/values-lv-rLV/strings.xml b/patches/src/main/resources/addresources/values-lv-rLV/strings.xml index 77874c64fc..117d853fed 100644 --- a/patches/src/main/resources/addresources/values-lv-rLV/strings.xml +++ b/patches/src/main/resources/addresources/values-lv-rLV/strings.xml @@ -57,7 +57,7 @@ Second \"item\" text" Nepieciešama restartēšana Lai šīs izmaiņas stātos spēkā, restartējiet lietotni. Restartēt - Importēt + Ievietot Kopēt ReVanced iestatījumi atiestatīti uz noklusējuma vērtībām Importēti %d iestatījumi @@ -641,8 +641,8 @@ Ierobežojumi: Jūsu instalētās ārējās lejupielādētāja lietotnes pakotnes nosaukums Ievadiet pakotnes nosaukumu Cits - Lietotne nav instalēta - %s nav instalēts. Lūdzu, instalējiet to. + Lietotne nav uzstādīta + %s nav uzstādīta. Lūdzu, uzstādiet to. "Nevarēja atrast instalēto lietotni ar pakotnes nosaukumu: %s Pārbaudiet, vai pakotnes nosaukums ir pareizs un lietotne ir instalēta" diff --git a/patches/src/main/resources/addresources/values-nl-rNL/strings.xml b/patches/src/main/resources/addresources/values-nl-rNL/strings.xml index 1a9e2785ac..a73508146f 100644 --- a/patches/src/main/resources/addresources/values-nl-rNL/strings.xml +++ b/patches/src/main/resources/addresources/values-nl-rNL/strings.xml @@ -25,7 +25,7 @@ Second \"item\" text" Aangepast App-pictogram Origineel - shared.layout.branding.baseCustomBrandingPatch.revanced_custom_branding_icon_entry_2 + ReVanced ReVanced minimaal ReVanced geschaald @@ -33,7 +33,7 @@ Second \"item\" text" Aangepast - Controle mislukt + Controles mislukt Open officiële website Negeren <h5>Deze app lijkt niet door u te zijn gepatcht.</h5><br>Deze app werkt mogelijk niet goed, is **mogelijk schadelijk of zelfs gevaarlijk om te gebruiken**.<br><br>Deze checks geven aan dat deze app is gepatcht of van iemand anders is verkregen:<br><br><small>%1$s</small><br>Het is sterk aan te raden om **deze app te verwijderen en zelf te patchen** om er zeker van te zijn dat u een gevalideerde en veilige app gebruikt.<p><br>Indien genegeerd, zal deze waarschuwing nog slechts twee keer worden getoond. @@ -1592,6 +1592,7 @@ Het inschakelen hiervan kan ontbrekende afbeeldingen oplossen die in sommige reg DeArrow & Oorspronkelijke miniaturen DeArrow & Stilstaande opnames Stilstaande opnames + DeArrow "DeArrow biedt door de menigte gecrowdsourcede miniaturen voor YouTube-video's. Deze miniaturen zijn vaak relevanter dan die van YouTube. Als dit is ingeschakeld, worden video-URL's naar de API-server verzonden en worden geen andere gegevens verzonden. Als een video geen DeArrow-miniaturen heeft, worden de originele of stilstaande opnames weergegeven. @@ -1635,7 +1636,11 @@ Tik hier om meer te weten te komen over DeArrow" Loopvideo is ingeschakeld Loopvideo is uitgeschakeld - + + Pauzeren bij audio-onderbreking + Afspelen wordt gepauzeerd wanneer andere audio wordt afgespeeld (bijv. navigatie) + Volume wordt zachter wanneer andere audio wordt afgespeeld + Spoof apparaatdimensies "Apparaatdimensies gespoofed @@ -1695,6 +1700,7 @@ Het inschakelen hiervan kan hogere videokwaliteiten ontgrendelen" Standaardkwaliteit voor Shorts op wifi-netwerk Standaardkwaliteit voor Shorts op mobiel netwerk mobiel + wifi Standaard %1$s-kwaliteit gewijzigd naar: %2$s De kwaliteit van Shorts %1$s is gewijzigd in: %2$s @@ -1789,7 +1795,11 @@ Het afspelen van video met AV1 kan haperen of frames overslaan." - + + YT Music ReVanced + Music ReVanced + Music + Over @@ -1916,6 +1926,7 @@ Het afspelen van video met AV1 kan haperen of frames overslaan." Over ReVanced Advertentieblokkering Instellingen advertentieblokkering + Chat Chat-instellingen Overige Diverse instellingen diff --git a/patches/src/main/resources/addresources/values-zh-rTW/strings.xml b/patches/src/main/resources/addresources/values-zh-rTW/strings.xml index d2fb68c602..e8b3937112 100644 --- a/patches/src/main/resources/addresources/values-zh-rTW/strings.xml +++ b/patches/src/main/resources/addresources/values-zh-rTW/strings.xml @@ -91,7 +91,7 @@ Second \"item\" text" ReVanced 語言 "部分語言的翻譯可能缺少或不完整。 -如要翻譯新的語言或改善現有翻譯,請前往 translate.revanced.app" +若要翻譯新的語言或改善現有翻譯,請前往 translate.revanced.app" 應用程式語言 匯入/匯出 匯入/匯出 ReVanced 設定 @@ -312,8 +312,8 @@ Second \"item\" text" 票券架已顯示 隱藏影片推薦標籤 - 搜尋結果中的「其他人也觀看」與「您可能也喜歡」標籤已隱藏 - 搜尋結果中的「其他人也觀看」與「您可能也喜歡」標籤已顯示 + 已隱藏搜尋結果中的「其他人也觀看」與「你可能也喜歡」標籤 + 已顯示搜尋結果中的「其他人也觀看」與「你可能也喜歡」標籤 隱藏視覺間隔 視覺間隔已隱藏 視覺間隔已顯示 @@ -907,9 +907,9 @@ Second \"item\" text" 隱藏影片畫質選單 影片畫質選單已隱藏 影片畫質選單已顯示 - 隱藏影片畫質選單頁尾 - 已隱藏影片畫質選單頁尾 - 已顯示影片畫質選單頁尾 + 隱藏「影片畫質」選單頁尾 + 已隱藏「影片畫質」選單頁尾 + 已顯示「影片畫質」選單頁尾 隱藏「自動播放」按鈕 @@ -1670,8 +1670,8 @@ Second \"item\" text" 拖曳還原觸覺回饋已停用 拖曳還原觸覺回饋已啟用 停用輕觸並按住觸覺回饋 - 輕觸並按住觸覺回饋已停用 - 輕觸並按住觸覺回饋已啟用 + 已停用輕觸並按住觸覺回饋 + 已啟用輕觸並按住觸覺回饋 停用縮放震動 縮放觸覺回饋已停用 縮放觸覺回饋已啟用 @@ -1712,12 +1712,12 @@ Second \"item\" text" 顯示速度對話方塊按鈕 - 速度對話框按鈕已顯示。長按可將播放速度重設為預設值 + 已顯示速度對話框按鈕。長按可將播放速度重設為預設值 速度對話框按鈕未顯示 顯示畫質切換按鈕 - 影片畫質按鈕已顯示。長按可重設為預設畫質 + 已顯示影片畫質按鈕。長按可重設為預設畫質 影片畫質按鈕未顯示 @@ -1791,7 +1791,7 @@ AV1 視訊播放可能會卡頓或丟格。" • 影片可能會在 1:00 停止,或在某些地區無法播放 • 音軌選單遺失 • 沒有 AV1 影片解碼器 - • 穩定音量無法使用 + • 無法使用平衡音量 • 在登出或無痕模式下,兒童影片可能無法播放 • 強制原始音訊不可用 diff --git a/patches/src/main/resources/addresources/values/arrays.xml b/patches/src/main/resources/addresources/values/arrays.xml index 230b9b8f38..e75cb6671a 100644 --- a/patches/src/main/resources/addresources/values/arrays.xml +++ b/patches/src/main/resources/addresources/values/arrays.xml @@ -220,14 +220,14 @@ + Android Reel Android VR visionOS - Android No SDK + ANDROID_REEL ANDROID_VR_1_43_32 VISIONOS - ANDROID_NO_SDK @@ -264,18 +264,16 @@ + Android Reel Android VR Android Studio - Android No SDK visionOS - iPadOS + ANDROID_REEL ANDROID_VR_1_43_32 ANDROID_CREATOR - ANDROID_NO_SDK VISIONOS - IPADOS @@ -663,8 +661,7 @@ cross-out - - + @string/revanced_block_embedded_ads_entry_1 diff --git a/patches/src/main/resources/addresources/values/strings.xml b/patches/src/main/resources/addresources/values/strings.xml index 95da75bad4..28a4d71661 100644 --- a/patches/src/main/resources/addresources/values/strings.xml +++ b/patches/src/main/resources/addresources/values/strings.xml @@ -144,6 +144,16 @@ If you are a YouTube Premium user, this setting may not be required" Playback may not work" Turning off this setting may cause playback issues. Default client + + • Experimental client and may stop working anytime + • Video may stop at 1:00, or may not be available in some regions + • Audio track menu is missing + • No AV1 video codec + • 360° VR immersive mode is not available + • Stable volume is not available + Spoofing side effects + + • Force original audio is not available Force original audio language @@ -900,10 +910,10 @@ Adjust volume by swiping vertically on the right side of the screen" Audio track menu is hidden Audio track menu is shown + 'Android Reel' must be kept untranslated. --> "Audio track menu is hidden -To show the Audio track menu, change \'Spoof video streams\' to \'Android No SDK\'" +To show the Audio track menu, change \'Spoof video streams\' to \'Android >Reel\'" Hide Watch in VR Watch in VR menu is hidden @@ -1786,18 +1796,9 @@ Playback may stutter or drop frames" "Enabling this setting may use software AV1 decoding. Video playback with AV1 may stutter or drop frames." - Spoofing side effects - • Experimental client and may stop working anytime - • Video may stop at 1:00, or may not be available in some regions - • Audio track menu is missing - • No AV1 video codec - • Stable volume is not available - • Kids videos may not play when logged out or in incognito mode - - • Force original audio is not available - Show in Stats for nerds - Client type is shown in Stats for nerds - Client is hidden in Stats for nerds + Show in Stats for nerds + Client type is shown in Stats for nerds + Client is hidden in Stats for nerds diff --git a/settings.gradle.kts b/settings.gradle.kts index 167fb51c8b..1a95f2d066 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -2,7 +2,6 @@ rootProject.name = "revanced-patches" pluginManagement { repositories { - mavenLocal() gradlePluginPortal() google() maven { @@ -10,12 +9,16 @@ pluginManagement { url = uri("https://maven.pkg.github.com/revanced/revanced-patches-gradle-plugin") credentials(PasswordCredentials::class) } + // TODO: Remove once https://github.com/google/protobuf-gradle-plugin/pull/797 is merged. + maven { url = uri("https://jitpack.io") } } -} - -dependencyResolutionManagement { - repositories { - mavenLocal() + // TODO: Remove once https://github.com/google/protobuf-gradle-plugin/pull/797 is merged. + resolutionStrategy { + eachPlugin { + if (requested.id.id == "com.google.protobuf") { + useModule("com.github.ReVanced:protobuf-gradle-plugin:${requested.version}") + } + } } } @@ -33,4 +36,4 @@ settings { } } -include(":patches:stub") +include(":patches:stub") \ No newline at end of file