From 252617b8dd3f24e1ff9a04ba1d91b43dc29bd757 Mon Sep 17 00:00:00 2001 From: PlayDay <18056374+playday3008@users.noreply.github.com> Date: Mon, 16 Mar 2026 12:07:09 +0100 Subject: [PATCH 01/17] fix(Export internal data documents provider): Correct S_IFLNK constant and symlink detection mask (#6819) --- .../documentsprovider/InternalDataDocumentsProvider.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/extensions/all/misc/directory/documentsprovider/export-internal-data-documents-provider/src/main/java/app/revanced/extension/all/misc/directory/documentsprovider/InternalDataDocumentsProvider.java b/extensions/all/misc/directory/documentsprovider/export-internal-data-documents-provider/src/main/java/app/revanced/extension/all/misc/directory/documentsprovider/InternalDataDocumentsProvider.java index f4e3a8e031..ad9d48f6ec 100644 --- a/extensions/all/misc/directory/documentsprovider/export-internal-data-documents-provider/src/main/java/app/revanced/extension/all/misc/directory/documentsprovider/InternalDataDocumentsProvider.java +++ b/extensions/all/misc/directory/documentsprovider/export-internal-data-documents-provider/src/main/java/app/revanced/extension/all/misc/directory/documentsprovider/InternalDataDocumentsProvider.java @@ -31,7 +31,10 @@ public class InternalDataDocumentsProvider extends DocumentsProvider { private static final String[] directoryColumns = {"document_id", "mime_type", "_display_name", "last_modified", "flags", "_size", "full_path", "lstat_info"}; - private static final int S_IFLNK = 0x8000; + @SuppressWarnings("OctalInteger") + private static final int S_IFMT = 0170000; + @SuppressWarnings("OctalInteger") + private static final int S_IFLNK = 0120000; private String packageName; private File dataDirectory; @@ -47,7 +50,7 @@ public class InternalDataDocumentsProvider extends DocumentsProvider { if (root.isDirectory()) { try { // Only delete recursively if the directory is not a symlink - if ((Os.lstat(root.getPath()).st_mode & S_IFLNK) != S_IFLNK) { + if ((Os.lstat(root.getPath()).st_mode & S_IFMT) != S_IFLNK) { File[] files = root.listFiles(); if (files != null) { for (File file : files) { @@ -324,7 +327,7 @@ public class InternalDataDocumentsProvider extends DocumentsProvider { sb.append(";"); sb.append(lstat.st_gid); // Append symlink target if it is a symlink - if ((lstat.st_mode & S_IFLNK) == S_IFLNK) { + if ((lstat.st_mode & S_IFMT) == S_IFLNK) { sb.append(";"); sb.append(Os.readlink(path)); } From 558f683bf227a63dbdd1760f052416b326758d72 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Mon, 16 Mar 2026 11:09:45 +0000 Subject: [PATCH 02/17] chore: Release v6.0.2-dev.1 [skip ci] ## [6.0.2-dev.1](https://github.com/ReVanced/revanced-patches/compare/v6.0.1...v6.0.2-dev.1) (2026-03-16) ### 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)) --- CHANGELOG.md | 7 +++++++ gradle.properties | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f558acff38..71a5255a80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [6.0.2-dev.1](https://github.com/ReVanced/revanced-patches/compare/v6.0.1...v6.0.2-dev.1) (2026-03-16) + + +### 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)) + ## [6.0.1](https://github.com/ReVanced/revanced-patches/compare/v6.0.0...v6.0.1) (2026-03-15) diff --git a/gradle.properties b/gradle.properties index f343fe4628..90c56c2c2f 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.0.1 +version = 6.0.2-dev.1 From 462702c174fe48de1d6ac970d1a16271034005bb Mon Sep 17 00:00:00 2001 From: PlayDay <18056374+playday3008@users.noreply.github.com> Date: Mon, 16 Mar 2026 12:26:00 +0100 Subject: [PATCH 03/17] fix(all/packagename): Fix package name validation and provider authority matching Allow uppercase letters in package name validation regex, since valid Java package names can start with uppercase. Broaden provider authority replacement to also match authorities that contain or end with the original package name, not just those prefixed by it. --- .../all/misc/packagename/ChangePackageNamePatch.kt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/patches/src/main/kotlin/app/revanced/patches/all/misc/packagename/ChangePackageNamePatch.kt b/patches/src/main/kotlin/app/revanced/patches/all/misc/packagename/ChangePackageNamePatch.kt index 271b2fc0fe..9fdeb04c38 100644 --- a/patches/src/main/kotlin/app/revanced/patches/all/misc/packagename/ChangePackageNamePatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/all/misc/packagename/ChangePackageNamePatch.kt @@ -1,6 +1,9 @@ package app.revanced.patches.all.misc.packagename -import app.revanced.patcher.patch.* +import app.revanced.patcher.patch.Option +import app.revanced.patcher.patch.booleanOption +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patcher.patch.stringOption import app.revanced.util.asSequence import app.revanced.util.getNode import org.w3c.dom.Element @@ -13,7 +16,7 @@ private val packageNameOption = stringOption( description = "The name of the package to rename the app to.", required = true, ) { - it == "Default" || it!!.matches(Regex("^[a-z]\\w*(\\.[a-z]\\w*)+\$")) + it == "Default" || it!!.matches(Regex("^[a-zA-Z]\\w*(\\.[a-zA-Z]\\w*)+$")) } /** @@ -111,7 +114,7 @@ val changePackageNamePatch = resourcePatch( val provider = node as Element val authorities = provider.getAttribute("android:authorities") - if (!authorities.startsWith("$packageName.")) continue + if ("$packageName." !in authorities && !authorities.endsWith(packageName)) continue provider.setAttribute("android:authorities", authorities.replace(packageName, newPackageName)) } From df1c3a4a70fd2595d77b539299f1f7301bc60d24 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Mon, 16 Mar 2026 22:01:49 +0100 Subject: [PATCH 04/17] feat: Change contact email in patches about The new dedicated email will is specialized for patches purposes, keeping it separate from organizational/ general emails. --- patches/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/patches/build.gradle.kts b/patches/build.gradle.kts index b0adaf6da6..87ac8faecd 100644 --- a/patches/build.gradle.kts +++ b/patches/build.gradle.kts @@ -6,7 +6,7 @@ patches { description = "Patches for ReVanced" source = "git@github.com:revanced/revanced-patches.git" author = "ReVanced" - contact = "contact@revanced.app" + contact = "patches@revanced.app" website = "https://revanced.app" license = "GNU General Public License v3.0" } From 64cae2bfa557309fa54166751567139bbdbd5205 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Mon, 16 Mar 2026 22:38:34 +0000 Subject: [PATCH 05/17] chore: Release v6.1.0-dev.1 [skip ci] # [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) ### Features * Change contact email in patches about ([df1c3a4](https://github.com/ReVanced/revanced-patches/commit/df1c3a4a70fd2595d77b539299f1f7301bc60d24)) --- CHANGELOG.md | 7 +++++++ gradle.properties | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 71a5255a80..822edfd6d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [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) + + +### Features + +* Change contact email in patches about ([df1c3a4](https://github.com/ReVanced/revanced-patches/commit/df1c3a4a70fd2595d77b539299f1f7301bc60d24)) + ## [6.0.2-dev.1](https://github.com/ReVanced/revanced-patches/compare/v6.0.1...v6.0.2-dev.1) (2026-03-16) diff --git a/gradle.properties b/gradle.properties index 90c56c2c2f..655da7c79e 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.0.2-dev.1 +version = 6.1.0-dev.1 From a05386e8bc24c085b5c74f3674c402c5dd5ad468 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Tue, 17 Mar 2026 13:16:40 +0100 Subject: [PATCH 06/17] feat(Announcements): Support ReVanced API v5 announcements --- .../announcements/AnnouncementsPatch.java | 55 ++++++++++++++++--- .../requests/AnnouncementsRoutes.java | 6 +- 2 files changed, 50 insertions(+), 11 deletions(-) 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() { } From 6564c7642fc9825307f829d981fef48dc59f5cb5 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 17 Mar 2026 12:19:34 +0000 Subject: [PATCH 07/17] chore: Release v6.1.0-dev.2 [skip ci] # [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)) --- CHANGELOG.md | 7 +++++++ gradle.properties | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 822edfd6d9..506debafa5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [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/gradle.properties b/gradle.properties index 655da7c79e..452d52478c 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-dev.2 From 90eebe082c9a86c398cc966b92d95f7dcdaa70d3 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Wed, 18 Mar 2026 14:56:59 +0100 Subject: [PATCH 08/17] chore: Sync translations from Crowdin --- .../addresources/values-cs-rCZ/strings.xml | 41 +++++++++++++++++-- .../addresources/values-de-rDE/strings.xml | 12 ++++-- .../addresources/values-el-rGR/strings.xml | 34 +++++---------- .../addresources/values-es-rES/strings.xml | 10 ++++- .../addresources/values-ga-rIE/strings.xml | 40 +++++++++--------- .../addresources/values-hr-rHR/strings.xml | 10 ++--- .../addresources/values-in-rID/strings.xml | 2 +- .../addresources/values-ja-rJP/strings.xml | 6 +-- .../addresources/values-ko-rKR/strings.xml | 2 +- .../addresources/values-lv-rLV/strings.xml | 6 +-- .../addresources/values-nl-rNL/strings.xml | 19 +++++++-- .../addresources/values-zh-rTW/strings.xml | 22 +++++----- 12 files changed, 125 insertions(+), 79 deletions(-) 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 影片解碼器 - • 穩定音量無法使用 + • 無法使用平衡音量 • 在登出或無痕模式下,兒童影片可能無法播放 • 強制原始音訊不可用 From 14ea61355d12501e2841a01e15bed79c60ba6b51 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Wed, 18 Mar 2026 15:00:13 +0100 Subject: [PATCH 09/17] ci: Make strings permissions writeable for processing --- .github/workflows/pull_strings.yml | 1 + 1 file changed, 1 insertion(+) 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 }} From 4b6c3e312328fbf6a1c7065e27d8ff04573e58be Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Wed, 18 Mar 2026 15:00:26 +0100 Subject: [PATCH 10/17] feat(Spoof video streams): Add Android Reel client to fix playback issues (#6830) Co-authored-by: oSumAtrIX Co-authored-by: inotia00 <108592928+inotia00@users.noreply.github.com> --- .../patches/spoof/SpoofVideoStreamsPatch.java | 4 +- .../extension/music/settings/Settings.java | 2 +- extensions/shared/library/build.gradle.kts | 4 +- .../shared/settings/BaseSettings.java | 2 +- .../extension/shared/spoof/ClientType.java | 108 +++++++---------- .../shared/spoof/SpoofVideoStreamsPatch.java | 5 +- .../shared/spoof/requests/PlayerRoutes.java | 26 ++++- .../spoof/requests/StreamingDataRequest.java | 110 ++++++++++++------ extensions/shared/protobuf/build.gradle.kts | 55 +++++++++ .../shared/innertube/player_response.proto | 42 +++++++ .../innertube/reel_item_watch_response.proto | 14 +++ .../patches/spoof/SpoofVideoStreamsPatch.java | 11 +- ...oofVideoStreamsSideEffectsPreference.java} | 34 +++--- gradle/libs.versions.toml | 8 ++ .../misc/spoof/SpoofVideoStreamsPatch.kt | 4 +- .../misc/spoof/SpoofVideoStreamsPatch.kt | 4 +- .../resources/addresources/values/arrays.xml | 13 +-- .../resources/addresources/values/strings.xml | 29 ++--- settings.gradle.kts | 17 +-- 19 files changed, 319 insertions(+), 173 deletions(-) create mode 100644 extensions/shared/protobuf/build.gradle.kts create mode 100644 extensions/shared/protobuf/src/main/proto/app/revanced/extension/shared/innertube/player_response.proto create mode 100644 extensions/shared/protobuf/src/main/proto/app/revanced/extension/shared/innertube/reel_item_watch_response.proto rename extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/{SpoofStreamingDataSideEffectsPreference.java => SpoofVideoStreamsSideEffectsPreference.java} (75%) 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/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/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/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/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/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 From 43688d06224270c0ab9c82be3c0f58b3b332f5e6 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Wed, 18 Mar 2026 14:04:42 +0000 Subject: [PATCH 11/17] chore: Release v6.1.0-dev.3 [skip ci] # [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)) --- CHANGELOG.md | 7 +++++++ gradle.properties | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 506debafa5..cbd1883bdf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [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) diff --git a/gradle.properties b/gradle.properties index 452d52478c..1ec45a3285 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.2 +version = 6.1.0-dev.3 From e51c5292c171325e7cfa0f5ee85474d9b3961a34 Mon Sep 17 00:00:00 2001 From: Dawid Krajcarz <80264606+drobotk@users.noreply.github.com> Date: Wed, 18 Mar 2026 16:38:30 +0100 Subject: [PATCH 12/17] fix(YouTube - Custom branding): Fix double icons and change default branding to ReVanced (#6806) --- .../shared/patches/CustomBrandingPatch.java | 6 +- .../branding/BaseCustomBrandingPatch.kt | 65 +++++++++++++------ 2 files changed, 47 insertions(+), 24 deletions(-) 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/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, ), ) From 4bc8c7c0f60a095533f07dc281f0320f8eb22f3c Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Wed, 18 Mar 2026 15:39:46 +0000 Subject: [PATCH 13/17] feat: Add `Spoof root of trust` and `Spoof keystore security level` patch (#6751) Co-authored-by: oSumAtrIX --- patches/api/patches.api | 8 + .../sim/spoof/SpoofSimProviderPatch.kt | 239 +++++++++++++++--- .../connectivity/wifi/spoof/SpoofWifiPatch.kt | 69 ++--- .../spoof/SpoofKeystoreSecurityLevelPatch.kt | 28 ++ .../all/misc/spoof/SpoofRootOfTrustPatch.kt | 70 +++++ 5 files changed, 347 insertions(+), 67 deletions(-) create mode 100644 patches/src/main/kotlin/app/revanced/patches/all/misc/spoof/SpoofKeystoreSecurityLevelPatch.kt create mode 100644 patches/src/main/kotlin/app/revanced/patches/all/misc/spoof/SpoofRootOfTrustPatch.kt diff --git a/patches/api/patches.api b/patches/api/patches.api index be251e84a5..0bc30b14f9 100644 --- a/patches/api/patches.api +++ b/patches/api/patches.api @@ -125,6 +125,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; } 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/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 From e19275fb7d60a0623f82127aeb4e5a242723c54c Mon Sep 17 00:00:00 2001 From: rospino74 <34315725+rospino74@users.noreply.github.com> Date: Wed, 18 Mar 2026 16:40:22 +0100 Subject: [PATCH 14/17] feat!: Add `Spoof Play Age Signals` patch (#6692) Co-authored-by: ADudeCalledLeo <7997354+Leo40Git@users.noreply.github.com> Co-authored-by: Dawid Krajcarz <80264606+drobotk@users.noreply.github.com> Co-authored-by: oSumAtrIX --- .../DisablePlayIntegrityPatch.java | 2 +- patches/api/patches.api | 6 +- .../DisablePlayIntegrity.kt | 4 +- .../all/misc/play/SpoofPlayAgeSignals.kt | 138 ++++++++++++++++++ 4 files changed, 146 insertions(+), 4 deletions(-) rename extensions/all/misc/disable-play-integrity/src/main/java/app/revanced/extension/{playintegrity => play}/DisablePlayIntegrityPatch.java (92%) rename patches/src/main/kotlin/app/revanced/patches/all/misc/{playintegrity => play}/DisablePlayIntegrity.kt (95%) create mode 100644 patches/src/main/kotlin/app/revanced/patches/all/misc/play/SpoofPlayAgeSignals.kt 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/patches/api/patches.api b/patches/api/patches.api index 0bc30b14f9..49548475b6 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 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), +} From 4b699da220e5d1527c390792b6228e2d9cffedb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aaron=20Mompi=C3=A9?= Date: Wed, 18 Mar 2026 16:41:12 +0100 Subject: [PATCH 15/17] feat(Instagram): Add `Enable location sticker redesign` patch (#6808) Co-authored-by: oSumAtrIX --- patches/api/patches.api | 4 ++++ .../EnableLocationStickerRedesignPatch.kt | 20 +++++++++++++++++++ .../story/locationsticker/Fingerprints.kt | 16 +++++++++++++++ 3 files changed, 40 insertions(+) create mode 100644 patches/src/main/kotlin/app/revanced/patches/instagram/story/locationsticker/EnableLocationStickerRedesignPatch.kt create mode 100644 patches/src/main/kotlin/app/revanced/patches/instagram/story/locationsticker/Fingerprints.kt diff --git a/patches/api/patches.api b/patches/api/patches.api index 49548475b6..4a8ed81323 100644 --- a/patches/api/patches.api +++ b/patches/api/patches.api @@ -377,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/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()) +} From 66b6c8c8ed78875cecb6d06652c58be55ed6c18d Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Wed, 18 Mar 2026 15:44:38 +0000 Subject: [PATCH 16/17] chore: Release v6.1.0-dev.4 [skip ci] # [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)) --- CHANGELOG.md | 13 +++++++++++++ gradle.properties | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cbd1883bdf..758b77b5e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +# [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) diff --git a/gradle.properties b/gradle.properties index 1ec45a3285..3252b5681c 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.3 +version = 6.1.0-dev.4 From b632d0f04239d49f14425f86f21784a526594518 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Wed, 18 Mar 2026 15:54:36 +0000 Subject: [PATCH 17/17] chore: Release v6.1.0 [skip ci] # [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)) --- CHANGELOG.md | 17 +++++++++++++++++ gradle.properties | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 758b77b5e5..bfebce7e69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,20 @@ +# [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) diff --git a/gradle.properties b/gradle.properties index 3252b5681c..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.4 +version = 6.1.0