Compare commits

..
16 Commits
Author SHA1 Message Date
whitehai11 937729130f feat: Full build successful + Spotify premium patch structure
Release / Release (push) Failing after 17m36s
2026-09-03 23:49:46 +02:00
whitehai11 b73ca08003 feat(Spotify): Add Premium spoof patch
Release / Release (push) Failing after 16m42s
2026-09-03 23:22:54 +02:00
whitehai11andClaude Sonnet 5 42622a1bca feat(Spotify): Add Remove skip restrictions patch
Adds an opt-in patch that forces the local Restrictions.disallowSkippingNextReasons()
getter (read by every skip button in the app) to always report an empty
set, and forces the rapid-skip cooldown tracker to always behave like it
does after the first-ever skip. This removes the free-tier "6 skips per
hour" cap and its toasts, and as a side effect also unlocks the skip
button while an ad is playing, since it reads the same restriction.

This is purely a client-side behavior change: it does not report a
Premium account to Spotify's backend, and does not block or mute ad
audio itself.

Verified against a real Spotify 9.1.78.2215 APK via revanced-cli: both
fingerprints resolve and the patch applies cleanly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NXRsEcWGqDhsCbfEdrfCdE
2026-09-03 20:50:51 +02:00
whitehai11andClaude Sonnet 5 37ef795322 fix(YouTube): Fix Custom settings strings misplacement and disable broken array-based list preferences
The Custom settings strings added in 97b4fc5b5 were accidentally placed
under the Twitch settings patch block instead of YouTube's, which broke
resource compilation entirely. Moved them to the correct block and added
the missing _summary_on/_summary_off pairs for the switch preferences.

Also comments out 3 ListPreferences (playback speed, video quality,
startup tab) that reference "${key}_entries"/"${key}_entry_values" array
resources: the addresources/values/strings.xml pipeline only extracts
<string> elements and silently drops <array>/<string-array>, so these
preferences had no backing entries and broke patching. Re-enabling them
needs real ArrayResource wiring through ListPreference(entries=,
entryValues=).

Found by running the patches through revanced-cli against a real
YouTube 21.34.248 APK.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NXRsEcWGqDhsCbfEdrfCdE
2026-09-03 20:50:40 +02:00
whitehai11andClaude Sonnet 5 97b4fc5b5a fix(YouTube): Update fingerprints and logic for YouTube 21.34, add custom settings
Release / Release (push) Failing after 16m39s
Reworks fingerprint matching across ads, GMS core, litho filter, player
controls, context hook, background playback, end screen cards, miniplayer,
and Shorts autoplay patches to keep working on YouTube 21.34, where several
underlying methods and resources changed. Also fixes video stream spoofing
to build the request URI via Uri.Builder instead of the removed toString()
path, and adds a new Custom settings screen with AMOLED/pure black player,
ambient mode, Shorts hiding, playback speed/quality defaults, debug video
info, custom app name, startup tab, and URL cleaning options.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NXRsEcWGqDhsCbfEdrfCdE
2026-09-03 19:50:50 +02:00
whitehai11andClaude Sonnet 5 7427432759 feat(YouTube): Add compatibility with YouTube 21.34.248
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NXRsEcWGqDhsCbfEdrfCdE
2026-09-03 19:50:33 +02:00
Ohan Melkonyan 015fe10a23 Merge branch 'main' into 'main'
Release / Release (push) Failing after 17m0s
feat(Strava): Add `Export route` patch

See merge request ReVanced/revanced-patches!6947
2026-05-31 19:35:10 +00:00
Lucas Martinati 9cb66197cf feat(Strava): Add Export route patch 2026-05-31 19:35:10 +00:00
semantic-release-bot b632d0f042 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))
2026-03-18 15:54:36 +00:00
fca2470990 chore: Merge branch dev to main (#6820)
Co-authored-by: PlayDay <18056374+playday3008@users.noreply.github.com>
Co-authored-by: semantic-release-bot <semantic-release-bot@martynus.net>
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
Co-authored-by: Dawid Krajcarz <80264606+drobotk@users.noreply.github.com>
Co-authored-by: Kofhisho <github@alphexo.dev>
Co-authored-by: rospino74 <34315725+rospino74@users.noreply.github.com>
Co-authored-by: ADudeCalledLeo <7997354+Leo40Git@users.noreply.github.com>
Co-authored-by: Aaron Mompié <github@aaronmompie.com>
Co-authored-by: inotia00 <108592928+inotia00@users.noreply.github.com>
2026-03-18 16:50:48 +01:00
semantic-release-bot 66b6c8c8ed 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))
2026-03-18 15:44:38 +00:00
Aaron MompiéandoSumAtrIX 4b699da220 feat(Instagram): Add Enable location sticker redesign patch (#6808)
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
2026-03-18 16:41:12 +01:00
e19275fb7d 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 <johan.melkonyan1@web.de>
2026-03-18 16:40:22 +01:00
KofhishoandoSumAtrIX 4bc8c7c0f6 feat: Add Spoof root of trust and Spoof keystore security level patch (#6751)
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
2026-03-18 16:39:46 +01:00
Dawid Krajcarz e51c5292c1 fix(YouTube - Custom branding): Fix double icons and change default branding to ReVanced (#6806) 2026-03-18 16:38:30 +01:00
semantic-release-bot 43688d0622 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))
2026-03-18 14:04:42 +00:00
61 changed files with 1463 additions and 335 deletions
@@ -0,0 +1,25 @@
package app.revanced.patches.spotify.premium.interceptors
import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Response
import okhttp3.Protocol
import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody
class ProductStateInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
if (request.url.encodedPath.contains("/product-state")) {
val json = "{\"product\":\"premium\",\"type\":\"premium\",\"expires\":\"2099-12-31\",\"offline\":true,\"high_bitrate\":true}"
return Response.Builder()
.request(request)
.protocol(Protocol.HTTP_1_1)
.code(200)
.message("OK")
.body(json.toResponseBody("application/json".toMediaType()))
.build()
}
return chain.proceed(request)
}
}
+37
View File
@@ -1,3 +1,40 @@
# [6.1.0](https://github.com/ReVanced/revanced-patches/compare/v6.0.1...v6.1.0) (2026-03-18)
### Bug Fixes
* **Export internal data documents provider:** Correct S_IFLNK constant and symlink detection mask ([#6819](https://github.com/ReVanced/revanced-patches/issues/6819)) ([252617b](https://github.com/ReVanced/revanced-patches/commit/252617b8dd3f24e1ff9a04ba1d91b43dc29bd757))
* **YouTube - Custom branding:** Fix double icons and change default branding to ReVanced ([#6806](https://github.com/ReVanced/revanced-patches/issues/6806)) ([e51c529](https://github.com/ReVanced/revanced-patches/commit/e51c5292c171325e7cfa0f5ee85474d9b3961a34))
### Features
* Add `Spoof root of trust` and `Spoof keystore security level` patch ([#6751](https://github.com/ReVanced/revanced-patches/issues/6751)) ([4bc8c7c](https://github.com/ReVanced/revanced-patches/commit/4bc8c7c0f60a095533f07dc281f0320f8eb22f3c))
* **Announcements:** Support ReVanced API v5 announcements ([a05386e](https://github.com/ReVanced/revanced-patches/commit/a05386e8bc24c085b5c74f3674c402c5dd5ad468))
* Change contact email in patches about ([df1c3a4](https://github.com/ReVanced/revanced-patches/commit/df1c3a4a70fd2595d77b539299f1f7301bc60d24))
* **Instagram:** Add `Enable location sticker redesign` patch ([#6808](https://github.com/ReVanced/revanced-patches/issues/6808)) ([4b699da](https://github.com/ReVanced/revanced-patches/commit/4b699da220e5d1527c390792b6228e2d9cffedb7))
* **Spoof video streams:** Add Android Reel client to fix playback issues ([#6830](https://github.com/ReVanced/revanced-patches/issues/6830)) ([4b6c3e3](https://github.com/ReVanced/revanced-patches/commit/4b6c3e312328fbf6a1c7065e27d8ff04573e58be))
# [6.1.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v6.1.0-dev.3...v6.1.0-dev.4) (2026-03-18)
### Bug Fixes
* **YouTube - Custom branding:** Fix double icons and change default branding to ReVanced ([#6806](https://github.com/ReVanced/revanced-patches/issues/6806)) ([e51c529](https://github.com/ReVanced/revanced-patches/commit/e51c5292c171325e7cfa0f5ee85474d9b3961a34))
### Features
* Add `Spoof root of trust` and `Spoof keystore security level` patch ([#6751](https://github.com/ReVanced/revanced-patches/issues/6751)) ([4bc8c7c](https://github.com/ReVanced/revanced-patches/commit/4bc8c7c0f60a095533f07dc281f0320f8eb22f3c))
* **Instagram:** Add `Enable location sticker redesign` patch ([#6808](https://github.com/ReVanced/revanced-patches/issues/6808)) ([4b699da](https://github.com/ReVanced/revanced-patches/commit/4b699da220e5d1527c390792b6228e2d9cffedb7))
# [6.1.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v6.1.0-dev.2...v6.1.0-dev.3) (2026-03-18)
### Features
* **Spoof video streams:** Add Android Reel client to fix playback issues ([#6830](https://github.com/ReVanced/revanced-patches/issues/6830)) ([4b6c3e3](https://github.com/ReVanced/revanced-patches/commit/4b6c3e312328fbf6a1c7065e27d8ff04573e58be))
# [6.1.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v6.1.0-dev.1...v6.1.0-dev.2) (2026-03-17) # [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)
@@ -1,4 +1,4 @@
package app.revanced.extension.playintegrity; package app.revanced.extension.play;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
@@ -114,7 +114,7 @@ public class CustomBrandingPatch {
/** /**
* Injection point. * Injection point.
* * <p>
* The total number of app name aliases, including dummy aliases. * The total number of app name aliases, including dummy aliases.
*/ */
private static int numberOfPresetAppNames() { private static int numberOfPresetAppNames() {
@@ -146,13 +146,13 @@ public class CustomBrandingPatch {
public static int getDefaultAppNameIndex() { public static int getDefaultAppNameIndex() {
return userProvidedCustomName() return userProvidedCustomName()
? numberOfPresetAppNames() ? numberOfPresetAppNames()
: 1; : 2;
} }
public static BrandingTheme getDefaultIconStyle() { public static BrandingTheme getDefaultIconStyle() {
return userProvidedCustomIcon() return userProvidedCustomIcon()
? BrandingTheme.CUSTOM ? BrandingTheme.CUSTOM
: BrandingTheme.ORIGINAL; : BrandingTheme.ROUNDED;
} }
/** /**
@@ -0,0 +1,88 @@
package app.revanced.extension.strava;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import app.revanced.extension.shared.Utils;
public final class AddRouteExportPatch {
public static void showExportDialog(Activity activity) {
try {
Intent intent = activity.getIntent();
if (intent == null) return;
Bundle extras = intent.getExtras();
if (extras == null) return;
String routeId = null;
// Scan all Intent extras to find the Route ID.
for (String key : extras.keySet()) {
Object val = extras.get(key);
if (val != null) {
String strVal = val.toString();
// Check if we have a "SavedRoute" or "SuggestedRoute" object.
if (val.getClass().getName().contains("SavedRoute")) {
try {
for (java.lang.reflect.Field field : val.getClass().getDeclaredFields()) {
field.setAccessible(true);
if (field.getType() == long.class) {
long id = field.getLong(val);
if (id > 0) {
routeId = String.valueOf(id);
break;
}
}
}
} catch (Exception e) {}
}
if (routeId != null) break;
// Fallback: regex search in the text content.
Matcher m = Pattern.compile("routes/([0-9]+)").matcher(strVal);
if (m.find()) {
routeId = m.group(1);
break;
}
}
}
if (routeId != null) {
final String finalRouteId = routeId;
final String finalUrl = "https://www.strava.com/routes/" + finalRouteId;
// Wait 500ms to allow Strava's BottomSheet to open,
// then display the popup on top of it.
new Handler(Looper.getMainLooper()).postDelayed(() -> {
if (activity.isDestroyed() || activity.isFinishing()) return;
new AlertDialog.Builder(activity)
.setTitle("Share options")
.setMessage("Do you want to export this route or use the share menu?")
.setPositiveButton("Export GPX", (dialog, which) -> {
Utils.openLink(finalUrl + "/export_gpx");
activity.finish();
})
.setNeutralButton("Export TCX", (dialog, which) -> {
Utils.openLink(finalUrl + "/export_tcx");
activity.finish();
})
.setNegativeButton("Share", (dialog, which) -> {
// Close the popup, Strava's share menu is already displayed underneath.
})
.show();
}, 500);
}
} catch (Exception e) {
// Silently ignore errors to avoid crashes on other types of shares.
}
}
}
@@ -0,0 +1,9 @@
package com.strava.routes.data;
import java.io.Serializable;
public class Route implements Serializable {
public String getId() {
return null;
}
}
@@ -236,6 +236,17 @@ public final class AdsFilter extends Filter {
return Settings.HIDE_GENERAL_ADS.get(); return Settings.HIDE_GENERAL_ADS.get();
} }
/**
* Injection point.
* 21.34+ the player overlay timely shelf handler no longer gets a dedicated method,
* so instead the result of the overlay type `String.equals` comparison is passed in.
*
* @param original Whether the overlay is the timely shelf (already being shown when not hiding ads).
*/
public static boolean allowAds(boolean original) {
return Settings.HIDE_GENERAL_ADS.get() ? false : original;
}
/** /**
* Injection point. * Injection point.
*/ */
+1 -1
View File
@@ -4,4 +4,4 @@ org.gradle.parallel = true
android.useAndroidX = true android.useAndroidX = true
android.uniquePackageNames = false android.uniquePackageNames = false
kotlin.code.style = official kotlin.code.style = official
version = 6.1.0-dev.2 version = 6.1.0
+1
View File
@@ -0,0 +1 @@
sdk.dir=/usr/lib/android-sdk
+17 -1
View File
@@ -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 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 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 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 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 public static final fun addResources (Lapp/revanced/patcher/patch/Patch;Lkotlin/jvm/functions/Function1;)Z
@@ -125,6 +129,14 @@ public final class app/revanced/patches/all/misc/spoof/EnableRomSignatureSpoofin
public static final fun getEnableROMSignatureSpoofingPatch ()Lapp/revanced/patcher/patch/Patch; public 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 final class app/revanced/patches/all/misc/targetSdk/SetTargetSdkVersion34Kt {
public static final fun getSetTargetSDKVersion34Patch ()Lapp/revanced/patcher/patch/Patch; public static final fun getSetTargetSDKVersion34Patch ()Lapp/revanced/patcher/patch/Patch;
} }
@@ -365,6 +377,10 @@ public final class app/revanced/patches/instagram/story/flipping/DisableStoryAut
public static final fun getDisableStoryAutoFlippingPatch ()Lapp/revanced/patcher/patch/Patch; public 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 final class app/revanced/patches/irplus/ad/RemoveAdsPatchKt {
public static final fun getRemoveAdsPatch ()Lapp/revanced/patcher/patch/Patch; public static final fun getRemoveAdsPatch ()Lapp/revanced/patcher/patch/Patch;
} }
@@ -6,7 +6,8 @@ import app.revanced.patcher.extensions.replaceInstruction
import app.revanced.patcher.patch.bytecodePatch import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patcher.patch.intOption import app.revanced.patcher.patch.intOption
import app.revanced.patcher.patch.stringOption 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.OneRegisterInstruction
import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
import com.android.tools.smali.dexlib2.iface.reference.MethodReference 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 }, 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") val networkCountryIso by isoCountryPatchOption("Network ISO country code")
@@ -61,46 +63,119 @@ val spoofSIMProviderPatch = bytecodePatch(
description = "The full name of the SIM operator.", 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( dependsOn(
transformInstructionsPatch( bytecodePatch {
filterMap = { _, _, instruction, instructionIndex -> apply {
if (instruction !is ReferenceInstruction) return@transformInstructionsPatch null fun generateRandomNumeric(length: Int) = (1..length).map { ('0'..'9').random() }.joinToString("")
val reference = instruction.reference as? MethodReference ?: return@transformInstructionsPatch null fun String?.computeSpoof(randomizer: () -> String): String? {
if (this.isNullOrBlank()) return null
val match = MethodCall.entries.firstOrNull { search -> if (this.equals("random", ignoreCase = true)) return randomizer()
MethodUtil.methodSignaturesMatch(reference, search.reference) return this
} ?: 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
} }
replacement?.let { instructionIndex to it }
}, // Calculate the Luhn checksum (mod 10) to generate a valid 15th digit, standard for IMEI numbers.
transform = ::transformMethodCall, // 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( private fun transformMethodCall(mutableMethod: MutableMethod, entry: Pair<Int, String>) {
mutableMethod: MutableMethod, val (index, value) = entry
entry: Pair<Int, String>, val nextInstr = mutableMethod.getInstruction<Instruction>(index + 1)
) {
val (instructionIndex, methodCallValue) = entry
// Get the register which would have contained the return value if (nextInstr.opcode.name != "move-result-object") {
val register = mutableMethod.getInstruction<OneRegisterInstruction>(instructionIndex + 1).registerA mutableMethod.replaceInstruction(index, "nop")
return
}
// Replace the move-result instruction with our fake value val register = (nextInstr as OneRegisterInstruction).registerA
mutableMethod.replaceInstruction( mutableMethod.replaceInstruction(index, "const-string v$register, \"$value\"")
instructionIndex + 1, mutableMethod.replaceInstruction(index + 1, "nop")
"const-string v$register, \"$methodCallValue\"",
)
} }
private enum class MethodCall( private enum class MethodCall(
@@ -154,4 +229,100 @@ private enum class MethodCall(
"Ljava/lang/String;", "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;"
)
)
} }
@@ -3,7 +3,7 @@ package app.revanced.patches.all.misc.connectivity.wifi.spoof
import app.revanced.patcher.patch.bytecodePatch import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patches.all.misc.transformation.IMethodCall import app.revanced.patches.all.misc.transformation.IMethodCall
import app.revanced.patches.all.misc.transformation.filterMapInstruction35c 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 = private const val EXTENSION_CLASS_DESCRIPTOR_PREFIX =
"Lapp/revanced/extension/all/misc/connectivity/wifi/spoof/SpoofWifiPatch" "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") extendWith("extensions/all/misc/connectivity/wifi/spoof/spoof-wifi.rve")
dependsOn( dependsOn(
transformInstructionsPatch( bytecodePatch {
filterMap = { classDef, _, instruction, instructionIndex -> apply {
filterMapInstruction35c<MethodCall>( forEachInstructionAsSequence(
EXTENSION_CLASS_DESCRIPTOR_PREFIX, match = { classDef, _, instruction, instructionIndex ->
classDef, filterMapInstruction35c<MethodCall>(
instruction, EXTENSION_CLASS_DESCRIPTOR_PREFIX,
instructionIndex, classDef,
) instruction,
}, instructionIndex,
transform = { method, entry -> )
val (methodType, instruction, instructionIndex) = entry },
methodType.replaceInvokeVirtualWithExtension( transform = { method, entry ->
EXTENSION_CLASS_DESCRIPTOR, val (methodType, instruction, instructionIndex) = entry
method, methodType.replaceInvokeVirtualWithExtension(
instruction, EXTENSION_CLASS_DESCRIPTOR,
instructionIndex, method,
) instruction,
}, instructionIndex,
), )
})
}
},
) )
} }
// Information about method calls we want to replace // Information about method calls we want to replace.
@Suppress("unused") @Suppress("unused")
private enum class MethodCall( private enum class MethodCall(
override val definedClassName: String, override val definedClassName: String,
@@ -89,13 +92,13 @@ private enum class MethodCall(
"Landroid/net/NetworkInfo;", "Landroid/net/NetworkInfo;",
"getState", "getState",
arrayOf(), arrayOf(),
"Landroid/net/NetworkInfo\$State;", $$"Landroid/net/NetworkInfo$State;",
), ),
GetDetailedState( GetDetailedState(
"Landroid/net/NetworkInfo;", "Landroid/net/NetworkInfo;",
"getDetailedState", "getDetailedState",
arrayOf(), arrayOf(),
"Landroid/net/NetworkInfo\$DetailedState;", $$"Landroid/net/NetworkInfo$DetailedState;",
), ),
IsActiveNetworkMetered( IsActiveNetworkMetered(
"Landroid/net/ConnectivityManager;", "Landroid/net/ConnectivityManager;",
@@ -132,7 +135,7 @@ private enum class MethodCall(
"registerBestMatchingNetworkCallback", "registerBestMatchingNetworkCallback",
arrayOf( arrayOf(
"Landroid/net/NetworkRequest;", "Landroid/net/NetworkRequest;",
"Landroid/net/ConnectivityManager\$NetworkCallback;", $$"Landroid/net/ConnectivityManager$NetworkCallback;",
"Landroid/os/Handler;", "Landroid/os/Handler;",
), ),
"V", "V",
@@ -140,19 +143,19 @@ private enum class MethodCall(
RegisterDefaultNetworkCallback1( RegisterDefaultNetworkCallback1(
"Landroid/net/ConnectivityManager;", "Landroid/net/ConnectivityManager;",
"registerDefaultNetworkCallback", "registerDefaultNetworkCallback",
arrayOf("Landroid/net/ConnectivityManager\$NetworkCallback;"), arrayOf($$"Landroid/net/ConnectivityManager$NetworkCallback;"),
"V", "V",
), ),
RegisterDefaultNetworkCallback2( RegisterDefaultNetworkCallback2(
"Landroid/net/ConnectivityManager;", "Landroid/net/ConnectivityManager;",
"registerDefaultNetworkCallback", "registerDefaultNetworkCallback",
arrayOf("Landroid/net/ConnectivityManager\$NetworkCallback;", "Landroid/os/Handler;"), arrayOf($$"Landroid/net/ConnectivityManager$NetworkCallback;", "Landroid/os/Handler;"),
"V", "V",
), ),
RegisterNetworkCallback1( RegisterNetworkCallback1(
"Landroid/net/ConnectivityManager;", "Landroid/net/ConnectivityManager;",
"registerNetworkCallback", "registerNetworkCallback",
arrayOf("Landroid/net/NetworkRequest;", "Landroid/net/ConnectivityManager\$NetworkCallback;"), arrayOf("Landroid/net/NetworkRequest;", $$"Landroid/net/ConnectivityManager$NetworkCallback;"),
"V", "V",
), ),
RegisterNetworkCallback2( RegisterNetworkCallback2(
@@ -166,7 +169,7 @@ private enum class MethodCall(
"registerNetworkCallback", "registerNetworkCallback",
arrayOf( arrayOf(
"Landroid/net/NetworkRequest;", "Landroid/net/NetworkRequest;",
"Landroid/net/ConnectivityManager\$NetworkCallback;", $$"Landroid/net/ConnectivityManager$NetworkCallback;",
"Landroid/os/Handler;", "Landroid/os/Handler;",
), ),
"V", "V",
@@ -174,13 +177,13 @@ private enum class MethodCall(
RequestNetwork1( RequestNetwork1(
"Landroid/net/ConnectivityManager;", "Landroid/net/ConnectivityManager;",
"requestNetwork", "requestNetwork",
arrayOf("Landroid/net/NetworkRequest;", "Landroid/net/ConnectivityManager\$NetworkCallback;"), arrayOf("Landroid/net/NetworkRequest;", $$"Landroid/net/ConnectivityManager$NetworkCallback;"),
"V", "V",
), ),
RequestNetwork2( RequestNetwork2(
"Landroid/net/ConnectivityManager;", "Landroid/net/ConnectivityManager;",
"requestNetwork", "requestNetwork",
arrayOf("Landroid/net/NetworkRequest;", "Landroid/net/ConnectivityManager\$NetworkCallback;", "I"), arrayOf("Landroid/net/NetworkRequest;", $$"Landroid/net/ConnectivityManager$NetworkCallback;", "I"),
"V", "V",
), ),
RequestNetwork3( RequestNetwork3(
@@ -188,7 +191,7 @@ private enum class MethodCall(
"requestNetwork", "requestNetwork",
arrayOf( arrayOf(
"Landroid/net/NetworkRequest;", "Landroid/net/NetworkRequest;",
"Landroid/net/ConnectivityManager\$NetworkCallback;", $$"Landroid/net/ConnectivityManager$NetworkCallback;",
"Landroid/os/Handler;", "Landroid/os/Handler;",
), ),
"V", "V",
@@ -204,7 +207,7 @@ private enum class MethodCall(
"requestNetwork", "requestNetwork",
arrayOf( arrayOf(
"Landroid/net/NetworkRequest;", "Landroid/net/NetworkRequest;",
"Landroid/net/ConnectivityManager\$NetworkCallback;", $$"Landroid/net/ConnectivityManager$NetworkCallback;",
"Landroid/os/Handler;", "Landroid/os/Handler;",
"I", "I",
), ),
@@ -213,7 +216,7 @@ private enum class MethodCall(
UnregisterNetworkCallback1( UnregisterNetworkCallback1(
"Landroid/net/ConnectivityManager;", "Landroid/net/ConnectivityManager;",
"unregisterNetworkCallback", "unregisterNetworkCallback",
arrayOf("Landroid/net/ConnectivityManager\$NetworkCallback;"), arrayOf($$"Landroid/net/ConnectivityManager$NetworkCallback;"),
"V", "V",
), ),
UnregisterNetworkCallback2( UnregisterNetworkCallback2(
@@ -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.extensions.replaceInstruction
import app.revanced.patcher.patch.bytecodePatch 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.immutable.reference.ImmutableMethodReference
import com.android.tools.smali.dexlib2.util.MethodUtil 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( private val CONTEXT_BIND_SERVICE_METHOD_REFERENCE = ImmutableMethodReference(
"Landroid/content/Context;", "Landroid/content/Context;",
@@ -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<MethodReference>()
?: 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<OneRegisterInstruction>(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),
}
@@ -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")
}
}
)
}
}
@@ -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",
),
}
@@ -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)
}
}
@@ -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())
}
@@ -3,7 +3,6 @@ package app.revanced.patches.shared.layout.branding
import app.revanced.com.android.tools.smali.dexlib2.mutable.MutableMethod import app.revanced.com.android.tools.smali.dexlib2.mutable.MutableMethod
import app.revanced.patcher.extensions.addInstruction import app.revanced.patcher.extensions.addInstruction
import app.revanced.patcher.extensions.getInstruction import app.revanced.patcher.extensions.getInstruction
import app.revanced.patcher.firstImmutableClassDef
import app.revanced.patcher.patch.* import app.revanced.patcher.patch.*
import app.revanced.patches.all.misc.packagename.setOrGetFallbackPackageName import app.revanced.patches.all.misc.packagename.setOrGetFallbackPackageName
import app.revanced.patches.all.misc.resources.addResources import app.revanced.patches.all.misc.resources.addResources
@@ -126,14 +125,14 @@ internal fun baseCustomBrandingPatch(
val getBuilderIndex = if (isYouTubeMusic) { val getBuilderIndex = if (isYouTubeMusic) {
// YT Music the field is not a plain object type. // YT Music the field is not a plain object type.
indexOfFirstInstructionOrThrow { indexOfFirstInstructionOrThrow {
getReference<FieldReference>()?.type == "Landroid/app/Notification\$Builder;" getReference<FieldReference>()?.type == $$"Landroid/app/Notification$Builder;"
} }
} else { } else {
// Find the field name of the notification builder. Field is an Object type. // Find the field name of the notification builder. Field is an Object type.
val builderCastIndex = indexOfFirstInstructionOrThrow { val builderCastIndex = indexOfFirstInstructionOrThrow {
val reference = getReference<TypeReference>() val reference = getReference<TypeReference>()
opcode == Opcode.CHECK_CAST && opcode == Opcode.CHECK_CAST &&
reference?.type == "Landroid/app/Notification\$Builder;" reference?.type == $$"Landroid/app/Notification$Builder;"
} }
indexOfFirstInstructionReversedOrThrow(builderCastIndex) { indexOfFirstInstructionReversedOrThrow(builderCastIndex) {
getReference<FieldReference>()?.type == "Ljava/lang/Object;" getReference<FieldReference>()?.type == "Ljava/lang/Object;"
@@ -148,11 +147,11 @@ internal fun baseCustomBrandingPatch(
).forEach { index -> ).forEach { index ->
addInstructionsAtControlFlowLabel( addInstructionsAtControlFlowLabel(
index, index,
""" $$"""
move-object/from16 v0, p0 move-object/from16 v0, p0
iget-object v0, v0, $builderFieldName iget-object v0, v0, $$builderFieldName
check-cast v0, Landroid/app/Notification${'$'}Builder; check-cast v0, Landroid/app/Notification$Builder;
invoke-static { v0 }, $EXTENSION_CLASS_DESCRIPTOR->setNotificationIcon(Landroid/app/Notification${'$'}Builder;)V invoke-static { v0 }, $$EXTENSION_CLASS_DESCRIPTOR->setNotificationIcon(Landroid/app/Notification$Builder;)V
""", """,
) )
} }
@@ -162,16 +161,37 @@ internal fun baseCustomBrandingPatch(
) )
afterDependents { 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. // 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. // and can only do that in the afterDependents block here.
// The UI preferences cannot be selectively added here, because the settings afterDependents block // 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. // 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), // 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. // and the non-functional in-app settings are removed on app startup by extension code.
if (customName != null || customIcon != null) { if (isRootInstall && (useCustomName || useCustomIcon)) {
if (setOrGetFallbackPackageName(originalAppPackageName) == originalAppPackageName) { Logger.getLogger(this::class.java.name).warning(
Logger.getLogger(this::class.java.name).warning( "Custom branding does not work with root installation. No changes applied."
"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, activityAliasNameWithIntents,
).childNodes ).childNodes
// The YT application name can appear in some places alongside the system // If user provides a custom icon, then change the application icon ('static' icon)
// YouTube app, such as the settings app list and in the "open with" file picker. // which shows as the push notification for some devices, in the app settings,
// Because the YouTube app cannot be completely uninstalled and only disabled, // and as the icon for the apk before installing.
// use a custom name for this situation to disambiguate which app is which. // This icon cannot be dynamically selected and this change must only be done if the
application.setAttribute( // user provides an icon otherwise there is no way to restore the original YouTube icon.
"android:label", if (useCustomIcon) {
"@string/revanced_custom_branding_name_entry_2", 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. val enabledIconIndex = if (useCustomIcon) iconStyleNames.size else 0 // 0 indexing.
for (appNameIndex in 1..numberOfPresetAppNames) { for (appNameIndex in 1..numberOfPresetAppNames) {
@@ -336,7 +359,7 @@ internal fun baseCustomBrandingPatch(
iconMipmapName = originalLauncherIconName, iconMipmapName = originalLauncherIconName,
appNameIndex = appNameIndex, appNameIndex = appNameIndex,
useCustomName = useCustomNameLabel, useCustomName = useCustomNameLabel,
enabled = (appNameIndex == 1), enabled = false,
intentFilters, intentFilters,
), ),
) )
@@ -31,14 +31,11 @@ internal val BytecodePatchContext.buildInitPlaybackRequestMethodMatch by composi
} }
internal val BytecodePatchContext.buildPlayerRequestURIMethodMatch by composingFirstMethod("key", "asig") { internal val BytecodePatchContext.buildPlayerRequestURIMethodMatch by composingFirstMethod("key", "asig") {
returnType("Ljava/lang/String;") returnType($$"Landroid/net/Uri$Builder;")
opcodes( opcodes(
Opcode.INVOKE_VIRTUAL, // Register holds player request URI. Opcode.INVOKE_VIRTUAL, // The last Uri$Builder mutating call. Register holds the player request Uri$Builder.
Opcode.MOVE_RESULT_OBJECT, Opcode.MOVE_RESULT_OBJECT,
Opcode.IPUT_OBJECT, Opcode.RETURN_OBJECT, // Returns the built player request Uri$Builder.
Opcode.IGET_OBJECT,
Opcode.MONITOR_EXIT,
Opcode.RETURN_OBJECT,
) )
} }
@@ -154,7 +151,7 @@ internal val BytecodePatchContext.nerdsStatsVideoFormatBuilderMethod by gettingF
returnType("Ljava/lang/String;") returnType("Ljava/lang/String;")
parameterTypes("L") parameterTypes("L")
instructions( instructions(
"codecs=\""(), ", codecs="(),
) )
} }
@@ -82,14 +82,19 @@ internal fun spoofVideoStreamsPatch(
// region Block /get_watch requests to fall back to /player requests. // region Block /get_watch requests to fall back to /player requests.
buildPlayerRequestURIMethodMatch.method.apply { buildPlayerRequestURIMethodMatch.method.apply {
val invokeToStringIndex = buildPlayerRequestURIMethodMatch[0] val returnIndex = buildPlayerRequestURIMethodMatch[-1]
val uriRegister = getInstruction<FiveRegisterInstruction>(invokeToStringIndex).registerC val builderRegister = getInstruction<OneRegisterInstruction>(returnIndex).registerA
val freeRegister = findFreeRegister(returnIndex, builderRegister)
addInstructions( addInstructions(
invokeToStringIndex, returnIndex,
""" $$"""
invoke-static { v$uriRegister }, $EXTENSION_CLASS_DESCRIPTOR->blockGetWatchRequest(Landroid/net/Uri;)Landroid/net/Uri; invoke-virtual { v$builderRegister }, Landroid/net/Uri$Builder;->build()Landroid/net/Uri;
move-result-object v$uriRegister move-result-object v$freeRegister
invoke-static { v$freeRegister }, $$EXTENSION_CLASS_DESCRIPTOR->blockGetWatchRequest(Landroid/net/Uri;)Landroid/net/Uri;
move-result-object v$freeRegister
invoke-virtual { v$freeRegister }, Landroid/net/Uri;->buildUpon()Landroid/net/Uri$Builder;
move-result-object v$builderRegister
""", """,
) )
} }
@@ -0,0 +1,37 @@
package app.revanced.patches.spotify.misc.skip
import app.revanced.patcher.*
import app.revanced.patcher.patch.BytecodePatchContext
import app.revanced.util.literal
import com.android.tools.smali.dexlib2.AccessFlags
// Getter on the locally-held player Restrictions model (deserialized from local player
// state, not fetched fresh from the server per call) that lists the reasons skipping to
// the next track is currently disallowed. A free-tier account gets a non-empty set here
// once the "6 skips per hour" cap is hit, and it is also checked while an ad is playing
// to decide whether the skip button is enabled. Every skip button in the app (Now Playing
// bar, ad track, notification, etc.) reads this same getter, so overriding it to always
// report "no restrictions" removes the skip cap and unlocks skipping ads, without ever
// telling the server the account's plan changed.
internal val BytecodePatchContext.disallowSkippingNextReasonsMethod by gettingFirstMethodDeclaratively {
name("disallowSkippingNextReasons")
accessFlags(AccessFlags.PUBLIC)
parameterTypes()
// Restrictions itself only declares the getter abstractly; skip past it to the
// AutoValue-generated implementation, which is the one with an actual method body.
custom { !AccessFlags.ABSTRACT.isSet(accessFlags) }
}
// Local (client-side) rapid-skip cooldown tracker: keeps the timestamps of the last 3
// skips in a LinkedList and returns 3 ("Slow down! You get 6 skips per hour.") if all 3
// happened within 10 seconds (0x2710 ms) of each other, 2 (first-ever-skip "You now have
// 6 skips per hour" toast) the very first time it's called, or 1 (no toast) otherwise.
internal val BytecodePatchContext.freeTierRapidSkipCounterMethodMatch by composingFirstMethod {
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
returnType("I")
parameterTypes()
instructions(
method { name == "elapsedRealtime" && returnType == "J" },
afterAtMost(50, 10000L()),
)
}
@@ -0,0 +1,50 @@
package app.revanced.patches.spotify.misc.skip
import app.revanced.patcher.extensions.addInstructions
import app.revanced.patcher.firstClassDef
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.util.returnEarly
import com.android.tools.smali.dexlib2.AccessFlags
@Suppress("unused")
val removeSkipRestrictionsPatch = bytecodePatch(
name = "Remove skip restrictions",
description = "Removes the free-tier \"6 skips per hour\" limit and its toasts/upsells, " +
"and unlocks the skip button while an ad is playing. This only changes local " +
"client-side behavior; it does not report a Premium account to Spotify.",
use = false,
) {
compatibleWith("com.spotify.music")
apply {
// Force the Restrictions getter that every skip button reads to always report
// that skipping is allowed, by having it construct and return an empty instance
// of its own (obfuscated) collection return type instead of the real field.
disallowSkippingNextReasonsMethod.apply {
val emptyCollectionType = returnType
val emptyCollectionFactoryMethod = firstClassDef(emptyCollectionType).methods.first {
AccessFlags.STATIC.isSet(it.accessFlags) &&
it.returnType == emptyCollectionType &&
it.parameterTypes.map(Any::toString) == listOf("I", "[Ljava/lang/Object;")
}
// Reuses "this" (p0) as a scratch register instead of requiring a second
// local, since everything below this early return becomes dead code.
addInstructions(
0,
"""
const/4 v0, 0x0
new-array p0, v0, [Ljava/lang/Object;
invoke-static { v0, p0 }, ${emptyCollectionFactoryMethod.definingClass}->${emptyCollectionFactoryMethod.name}(I[Ljava/lang/Object;)$emptyCollectionType
move-result-object v0
return-object v0
""",
)
}
// Force the rapid-skip cooldown tracker to always behave like it does after the
// first-ever skip (return 1, no toast), instead of counting towards the cap.
freeTierRapidSkipCounterMethodMatch.method.returnEarly(1)
}
}
@@ -0,0 +1,25 @@
package app.revanced.patches.strava.route.export
import app.revanced.patcher.extensions.addInstructions
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patches.strava.misc.extension.sharedExtensionPatch
private const val ROUTE_EXPORT_CLASS_DESCRIPTOR = "Lapp/revanced/extension/strava/AddRouteExportPatch;"
@Suppress("unused")
val addRouteExportPatch = bytecodePatch(
name = "Add route export",
description = "Extends the route menu with items to export them in GPX or TCX formats.",
) {
compatibleWith("com.strava")
dependsOn(sharedExtensionPatch)
apply {
// Intercept onCreate to display the export dialog over.
shareSheetActivityOnCreateMethod.addInstructions(
0,
"invoke-static {p0}, $ROUTE_EXPORT_CLASS_DESCRIPTOR->showExportDialog(Landroid/app/Activity;)V"
)
}
}
@@ -0,0 +1,11 @@
package app.revanced.patches.strava.route.export
import app.revanced.patcher.definingClass
import app.revanced.patcher.gettingFirstMethodDeclaratively
import app.revanced.patcher.name
import app.revanced.patcher.patch.BytecodePatchContext
internal val BytecodePatchContext.shareSheetActivityOnCreateMethod by gettingFirstMethodDeclaratively {
name("onCreate")
definingClass("Lcom/strava/sharing/view/ShareSheetActivity;")
}
@@ -71,3 +71,27 @@ internal val BytecodePatchContext.playerOverlayTimelyShelfMethod by gettingFirst
) )
} }
/**
* 21.34+ removed the [String]"innertube_cue_range", "Null id" and "Null onExitActions" strings
* from the player overlay timely shelf handler, and the handler moved into a synthetic lambda
* (`a(Ljava/lang/Object;)V`) that dispatches on the overlay type via a `String.equals` comparison.
* Only [String]"player_overlay_timely_shelf" remains, so match on it plus the following
* `String.equals`/`move-result` used to check if the overlay is the timely shelf.
*/
internal val BytecodePatchContext.playerOverlayTimelyShelfMethod21_34 by composingFirstMethod {
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
returnType("V")
parameterTypes("Ljava/lang/Object;")
instructions(
"player_overlay_timely_shelf"(),
afterAtMost(
5,
allOf(
Opcode.INVOKE_VIRTUAL(),
method { name == "equals" && definingClass == "Ljava/lang/String;" },
),
),
after(Opcode.MOVE_RESULT()),
)
}
@@ -24,6 +24,7 @@ import app.revanced.patches.youtube.misc.engagement.engagementPanelHookPatch
import app.revanced.patches.youtube.misc.fix.backtoexitgesture.fixBackToExitGesturePatch import app.revanced.patches.youtube.misc.fix.backtoexitgesture.fixBackToExitGesturePatch
import app.revanced.patches.youtube.misc.litho.filter.lithoFilterPatch import app.revanced.patches.youtube.misc.litho.filter.lithoFilterPatch
import app.revanced.patches.youtube.misc.playservice.is_20_14_or_greater import app.revanced.patches.youtube.misc.playservice.is_20_14_or_greater
import app.revanced.patches.youtube.misc.playservice.is_21_34_or_greater
import app.revanced.patches.youtube.misc.playservice.versionCheckPatch import app.revanced.patches.youtube.misc.playservice.versionCheckPatch
import app.revanced.patches.youtube.misc.settings.PreferenceScreen import app.revanced.patches.youtube.misc.settings.PreferenceScreen
import app.revanced.patches.youtube.misc.settings.settingsPatch import app.revanced.patches.youtube.misc.settings.settingsPatch
@@ -34,6 +35,7 @@ import app.revanced.util.indexOfFirstInstructionReversedOrThrow
import app.revanced.util.injectHideViewCall import app.revanced.util.injectHideViewCall
import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction
private const val EXTENSION_CLASS_DESCRIPTOR = private const val EXTENSION_CLASS_DESCRIPTOR =
@@ -94,7 +96,8 @@ val hideAdsPatch = bytecodePatch(
"20.26.46", "20.26.46",
"20.31.42", "20.31.42",
"20.37.48", "20.37.48",
"20.40.45" "20.40.45",
"21.34.248"
), ),
) )
@@ -154,17 +157,41 @@ val hideAdsPatch = bytecodePatch(
// Hide player overlay view. This can be hidden with a regular litho filter // Hide player overlay view. This can be hidden with a regular litho filter
// but an empty space remains. // but an empty space remains.
if (is_20_14_or_greater) { if (is_20_14_or_greater) {
playerOverlayTimelyShelfMethod.addInstructionsWithLabels( if (is_21_34_or_greater) {
0, // 21.34+ the overlay handler moved into a synthetic lambda `a(Object)V`
""" // that dispatches on overlay type. Hook right after the `String.equals` /
invoke-static {}, ${EXTENSION_CLASS_DESCRIPTOR}->hideAds()Z // `move-result` that determines the overlay is the timely shelf so that
move-result v0 // only the timely shelf is hidden and not every player overlay.
if-eqz v0, :show playerOverlayTimelyShelfMethod21_34.let { match ->
return-void val insertIndex = match[-1]
:show val equalsResultRegister =
nop match.method.getInstruction<OneRegisterInstruction>(insertIndex).registerA
"""
) match.method.addInstructionsWithLabels(
insertIndex + 1,
"""
invoke-static { v$equalsResultRegister }, ${EXTENSION_CLASS_DESCRIPTOR}->allowAds(Z)Z
move-result v$equalsResultRegister
if-nez v$equalsResultRegister, :show
return-void
:show
nop
""",
)
}
} else {
playerOverlayTimelyShelfMethod.addInstructionsWithLabels(
0,
"""
invoke-static {}, ${EXTENSION_CLASS_DESCRIPTOR}->hideAds()Z
move-result v0
if-eqz v0, :show
return-void
:show
nop
"""
)
}
} }
@@ -29,7 +29,8 @@ val videoAdsPatch = bytecodePatch(
"20.26.46", "20.26.46",
"20.31.42", "20.31.42",
"20.37.48", "20.37.48",
"20.40.45" "20.40.45",
"21.34.248"
), ),
) )
@@ -31,7 +31,8 @@ val disableAutoCaptionsPatch = bytecodePatch(
"20.26.46", "20.26.46",
"20.31.42", "20.31.42",
"20.37.48", "20.37.48",
"20.40.45" "20.40.45",
"21.34.248"
), ),
) )
@@ -31,7 +31,8 @@ val customBrandingPatch = baseCustomBrandingPatch(
"20.26.46", "20.26.46",
"20.31.42", "20.31.42",
"20.37.48", "20.37.48",
"20.40.45" "20.40.45",
"21.34.248"
), ),
) )
}, },
@@ -39,7 +39,8 @@ val hideAutoplayPreviewPatch = bytecodePatch(
"20.26.46", "20.26.46",
"20.31.42", "20.31.42",
"20.37.48", "20.37.48",
"20.40.45" "20.40.45",
"21.34.248"
) )
) )
@@ -13,6 +13,7 @@ import app.revanced.patches.shared.misc.mapping.resourceMappingPatch
import app.revanced.patches.shared.misc.settings.preference.SwitchPreference import app.revanced.patches.shared.misc.settings.preference.SwitchPreference
import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch
import app.revanced.patches.youtube.misc.playservice.is_19_43_or_greater import app.revanced.patches.youtube.misc.playservice.is_19_43_or_greater
import app.revanced.patches.youtube.misc.playservice.is_21_34_or_greater
import app.revanced.patches.youtube.misc.playservice.versionCheckPatch import app.revanced.patches.youtube.misc.playservice.versionCheckPatch
import app.revanced.patches.youtube.misc.settings.PreferenceScreen import app.revanced.patches.youtube.misc.settings.PreferenceScreen
import app.revanced.patches.youtube.misc.settings.settingsPatch import app.revanced.patches.youtube.misc.settings.settingsPatch
@@ -68,7 +69,8 @@ val hideEndScreenCardsPatch = bytecodePatch(
"20.26.46", "20.26.46",
"20.31.42", "20.31.42",
"20.37.48", "20.37.48",
"20.40.45" "20.40.45",
"21.34.248"
), ),
) )
@@ -90,7 +92,11 @@ val hideEndScreenCardsPatch = bytecodePatch(
} }
} }
if (is_19_43_or_greater) { // 21.34+ the end screen cards are no longer shown through a dedicated 5-method
// container class that the legacy showEndscreenCardsMethod fingerprint can locate.
// Instead each card view (circle/icon/video) is created and hidden individually by the
// three layout fingerprints hooked above, which cover hiding the whole card on 21.34.
if (is_19_43_or_greater && !is_21_34_or_greater) {
showEndscreenCardsMethod.addInstructionsWithLabels( showEndscreenCardsMethod.addInstructionsWithLabels(
0, 0,
""" """
@@ -36,7 +36,8 @@ val hideEndScreenSuggestedVideoPatch = bytecodePatch(
"20.26.46", "20.26.46",
"20.31.42", "20.31.42",
"20.37.48", "20.37.48",
"20.40.45" "20.40.45",
"21.34.248"
), ),
) )
@@ -35,7 +35,8 @@ val disableFullscreenAmbientModePatch = bytecodePatch(
"20.26.46", "20.26.46",
"20.31.42", "20.31.42",
"20.37.48", "20.37.48",
"20.40.45" "20.40.45",
"21.34.248"
), ),
) )
@@ -31,7 +31,8 @@ val disablePlayerPopupPanelsPatch = bytecodePatch(
"20.26.46", "20.26.46",
"20.31.42", "20.31.42",
"20.37.48", "20.37.48",
"20.40.45" "20.40.45",
"21.34.248"
), ),
) )
@@ -35,7 +35,8 @@ val hideRelatedVideoOverlayPatch = bytecodePatch(
"20.26.46", "20.26.46",
"20.31.42", "20.31.42",
"20.37.48", "20.37.48",
"20.40.45" "20.40.45",
"21.34.248"
), ),
) )
@@ -35,7 +35,8 @@ val disableRollingNumberAnimationsPatch = bytecodePatch(
"20.26.46", "20.26.46",
"20.31.42", "20.31.42",
"20.37.48", "20.37.48",
"20.40.45" "20.40.45",
"21.34.248"
), ),
) )
@@ -32,7 +32,8 @@ val disableSignInToTVPopupPatch = bytecodePatch(
"20.26.46", "20.26.46",
"20.31.42", "20.31.42",
"20.37.48", "20.37.48",
"20.40.45" "20.40.45",
"21.34.248"
), ),
) )
@@ -30,7 +30,8 @@ val hideTimestampPatch = bytecodePatch(
"20.26.46", "20.26.46",
"20.31.42", "20.31.42",
"20.37.48", "20.37.48",
"20.40.45" "20.40.45",
"21.34.248"
), ),
) )
@@ -92,8 +92,15 @@ val miniplayerPatch = bytecodePatch(
val preferences = mutableSetOf<BasePreference>() val preferences = mutableSetOf<BasePreference>()
preferences += preferences +=
if (is_20_37_or_greater) { if (is_20_37_or_greater && !is_21_34_or_greater) {
ListPreference("revanced_miniplayer_type") ListPreference("revanced_miniplayer_type")
} else if (is_21_34_or_greater) {
// YouTube 21.29+ removed all modern miniplayers except "Modern 4",
// so the type picker no longer has any effect.
NonInteractivePreference(
key = "revanced_miniplayer_type",
summaryKey = "revanced_miniplayer_type_not_available_summary",
)
} else if (is_20_03_or_greater) { } else if (is_20_03_or_greater) {
ListPreference( ListPreference(
key = "revanced_miniplayer_type", key = "revanced_miniplayer_type",
@@ -153,7 +160,9 @@ val miniplayerPatch = bytecodePatch(
) )
} }
preferences += TextPreference("revanced_miniplayer_opacity", inputType = InputType.NUMBER) if (!is_21_34_or_greater) {
preferences += TextPreference("revanced_miniplayer_opacity", inputType = InputType.NUMBER)
}
PreferenceScreen.PLAYER.addPreferences( PreferenceScreen.PLAYER.addPreferences(
PreferenceScreenPreference( PreferenceScreenPreference(
@@ -265,101 +274,113 @@ val miniplayerPatch = bytecodePatch(
// region Enable modern miniplayer. // region Enable modern miniplayer.
miniplayerModernConstructorMethod.classDef.methods.forEach { // Select which modern miniplayer type is used.
it.apply { // YouTube 21.29+ removed all modern miniplayers except "Modern 4", so there is
if (AccessFlags.CONSTRUCTOR.isSet(accessFlags)) { // nothing to choose between and this override is dead code.
val iPutIndex = indexOfFirstInstructionOrThrow { if (!is_21_34_or_greater) {
this.opcode == Opcode.IPUT && this.getReference<FieldReference>()?.type == "I" miniplayerModernConstructorMethod.classDef.methods.forEach {
} it.apply {
if (AccessFlags.CONSTRUCTOR.isSet(accessFlags)) {
val iPutIndex = indexOfFirstInstructionOrThrow {
this.opcode == Opcode.IPUT && this.getReference<FieldReference>()?.type == "I"
}
insertModernMiniplayerTypeOverride(iPutIndex) insertModernMiniplayerTypeOverride(iPutIndex)
} else { } else {
findReturnIndicesReversed().forEach { index -> findReturnIndicesReversed().forEach { index ->
insertModernMiniplayerOverride( insertModernMiniplayerOverride(
index index
) )
}
} }
} }
} }
} }
if (is_19_23_or_greater) { // Modern miniplayer feature-flag overrides.
miniplayerModernConstructorMethod.insertMiniplayerFeatureFlagBooleanOverride( // YouTube 21.29+ removed all modern miniplayers except "Modern 4" and moved/removed the
MINIPLAYER_DRAG_DROP_FEATURE_KEY, // feature flags for drag & drop, double-tap, rounded corners, horizontal drag and animated
"getMiniplayerDragAndDrop", // expand. The default/minimum size flags were removed in 21.32+. The fingerprints below
) // therefore no longer resolve on 21.34+, so these overrides only apply to older versions.
} if (!is_21_34_or_greater) {
if (is_19_23_or_greater) {
if (is_19_25_or_greater) { miniplayerModernConstructorMethod.insertMiniplayerFeatureFlagBooleanOverride(
miniplayerModernConstructorMethod.insertMiniplayerFeatureFlagBooleanOverride( MINIPLAYER_DRAG_DROP_FEATURE_KEY,
MINIPLAYER_MODERN_FEATURE_LEGACY_KEY, "getMiniplayerDragAndDrop",
"getModernMiniplayerOverride",
)
miniplayerModernConstructorMethod.insertMiniplayerFeatureFlagBooleanOverride(
MINIPLAYER_MODERN_FEATURE_KEY,
"getModernFeatureFlagsActiveOverride",
)
miniplayerModernConstructorMethod.insertMiniplayerFeatureFlagBooleanOverride(
MINIPLAYER_DOUBLE_TAP_FEATURE_KEY,
"getMiniplayerDoubleTapAction",
)
}
if (is_19_26_or_greater) {
miniplayerModernConstructorMethod.apply {
val literalIndex = indexOfFirstLiteralInstructionOrThrow(
MINIPLAYER_INITIAL_SIZE_FEATURE_KEY,
)
val targetIndex = indexOfFirstInstructionOrThrow(literalIndex, Opcode.LONG_TO_INT)
val register = getInstruction<OneRegisterInstruction>(targetIndex).registerA
addInstructions(
targetIndex + 1,
"""
invoke-static { v$register }, $EXTENSION_CLASS_DESCRIPTOR->getMiniplayerDefaultSize(I)I
move-result v$register
""",
) )
} }
// Override a minimum size constant. if (is_19_25_or_greater) {
miniplayerMinimumSizeMethodMatch.let { miniplayerModernConstructorMethod.insertMiniplayerFeatureFlagBooleanOverride(
it.method.apply { MINIPLAYER_MODERN_FEATURE_LEGACY_KEY,
val index = it[1] "getModernMiniplayerOverride",
val register = getInstruction<OneRegisterInstruction>(index).registerA )
// Smaller sizes can be used, but the miniplayer will always start in size 170 if set any smaller. miniplayerModernConstructorMethod.insertMiniplayerFeatureFlagBooleanOverride(
// The 170 initial limit probably could be patched to allow even smaller initial sizes, MINIPLAYER_MODERN_FEATURE_KEY,
// but 170 is already half the horizontal space and smaller does not seem useful. "getModernFeatureFlagsActiveOverride",
replaceInstruction(index, "const/16 v$register, 170") )
miniplayerModernConstructorMethod.insertMiniplayerFeatureFlagBooleanOverride(
MINIPLAYER_DOUBLE_TAP_FEATURE_KEY,
"getMiniplayerDoubleTapAction",
)
}
if (is_19_26_or_greater) {
miniplayerModernConstructorMethod.apply {
val literalIndex = indexOfFirstLiteralInstructionOrThrow(
MINIPLAYER_INITIAL_SIZE_FEATURE_KEY,
)
val targetIndex = indexOfFirstInstructionOrThrow(literalIndex, Opcode.LONG_TO_INT)
val register = getInstruction<OneRegisterInstruction>(targetIndex).registerA
addInstructions(
targetIndex + 1,
"""
invoke-static { v$register }, $EXTENSION_CLASS_DESCRIPTOR->getMiniplayerDefaultSize(I)I
move-result v$register
""",
)
}
// Override a minimum size constant.
miniplayerMinimumSizeMethodMatch.let {
it.method.apply {
val index = it[1]
val register = getInstruction<OneRegisterInstruction>(index).registerA
// Smaller sizes can be used, but the miniplayer will always start in size 170 if set any smaller.
// The 170 initial limit probably could be patched to allow even smaller initial sizes,
// but 170 is already half the horizontal space and smaller does not seem useful.
replaceInstruction(index, "const/16 v$register, 170")
}
} }
} }
}
if (is_19_36_or_greater) { if (is_19_36_or_greater) {
miniplayerModernConstructorMethod.insertMiniplayerFeatureFlagBooleanOverride( miniplayerModernConstructorMethod.insertMiniplayerFeatureFlagBooleanOverride(
MINIPLAYER_ROUNDED_CORNERS_FEATURE_KEY, MINIPLAYER_ROUNDED_CORNERS_FEATURE_KEY,
"getRoundedCorners", "getRoundedCorners",
) )
} }
if (is_19_43_or_greater) { if (is_19_43_or_greater) {
miniplayerOnCloseHandlerMethod.insertMiniplayerFeatureFlagBooleanOverride( miniplayerOnCloseHandlerMethod.insertMiniplayerFeatureFlagBooleanOverride(
MINIPLAYER_DISABLED_FEATURE_KEY, MINIPLAYER_DISABLED_FEATURE_KEY,
"getMiniplayerOnCloseHandler", "getMiniplayerOnCloseHandler",
) )
miniplayerModernConstructorMethod.insertMiniplayerFeatureFlagBooleanOverride( miniplayerModernConstructorMethod.insertMiniplayerFeatureFlagBooleanOverride(
MINIPLAYER_HORIZONTAL_DRAG_FEATURE_KEY, MINIPLAYER_HORIZONTAL_DRAG_FEATURE_KEY,
"getHorizontalDrag", "getHorizontalDrag",
) )
miniplayerModernConstructorMethod.insertMiniplayerFeatureFlagBooleanOverride( miniplayerModernConstructorMethod.insertMiniplayerFeatureFlagBooleanOverride(
MINIPLAYER_ANIMATED_EXPAND_FEATURE_KEY, MINIPLAYER_ANIMATED_EXPAND_FEATURE_KEY,
"getMaximizeAnimation", "getMaximizeAnimation",
) )
}
} }
// endregion // endregion
@@ -391,7 +412,8 @@ val miniplayerPatch = bytecodePatch(
// region fix minimal miniplayer using the wrong pause/play bold icons. // region fix minimal miniplayer using the wrong pause/play bold icons.
if (is_20_31_or_greater) { // 21.34 does not have the yt_fill_pause_* drawables this fingerprint anchors on.
if (is_20_31_or_greater && !is_21_34_or_greater) {
miniplayerSetIconsMethod.apply { miniplayerSetIconsMethod.apply {
findInstructionIndicesReversedOrThrow { findInstructionIndicesReversedOrThrow {
val reference = getReference<MethodReference>() val reference = getReference<MethodReference>()
@@ -421,7 +443,6 @@ val miniplayerPatch = bytecodePatch(
ClassDef::miniplayerModernActionButtonMethodMatch to "hideMiniplayerActionButton", ClassDef::miniplayerModernActionButtonMethodMatch to "hideMiniplayerActionButton",
ClassDef::miniplayerModernRewindButtonMethodMatch to "hideMiniplayerRewindForward", ClassDef::miniplayerModernRewindButtonMethodMatch to "hideMiniplayerRewindForward",
ClassDef::miniplayerModernForwardButtonMethodMatch to "hideMiniplayerRewindForward", ClassDef::miniplayerModernForwardButtonMethodMatch to "hideMiniplayerRewindForward",
ClassDef::miniplayerModernOverlayViewMethodMatch to "adjustMiniplayerOpacity",
).forEach { (matching, methodName) -> ).forEach { (matching, methodName) ->
val match = matching.get(miniplayerModernViewParentMethod.immutableClassDef) val match = matching.get(miniplayerModernViewParentMethod.immutableClassDef)
@@ -436,6 +457,23 @@ val miniplayerPatch = bytecodePatch(
} }
} }
// Adjust the overlay opacity. The modern overlay was restructured in 21.29+ alongside the
// removal of the default/minimum size flags, so this hook is only applied on older versions.
if (!is_21_34_or_greater) {
val match = ClassDef::miniplayerModernOverlayViewMethodMatch
.get(miniplayerModernViewParentMethod.immutableClassDef)
match.method.apply {
val index = match[-1]
val register = getInstruction<OneRegisterInstruction>(index).registerA
addInstruction(
index + 1,
"invoke-static { v$register }, $EXTENSION_CLASS_DESCRIPTOR->adjustMiniplayerOpacity(Landroid/view/View;)V",
)
}
}
miniplayerModernViewParentMethod.immutableClassDef.getMiniplayerModernAddViewListenerMethod() miniplayerModernViewParentMethod.immutableClassDef.getMiniplayerModernAddViewListenerMethod()
.addInstruction( .addInstruction(
0, 0,
@@ -15,6 +15,7 @@ import app.revanced.patches.shared.misc.settings.preference.SwitchPreference
import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch
import app.revanced.patches.youtube.misc.playservice.is_19_34_or_greater import app.revanced.patches.youtube.misc.playservice.is_19_34_or_greater
import app.revanced.patches.youtube.misc.playservice.is_20_09_or_greater import app.revanced.patches.youtube.misc.playservice.is_20_09_or_greater
import app.revanced.patches.youtube.misc.playservice.is_21_34_or_greater
import app.revanced.patches.youtube.misc.playservice.versionCheckPatch import app.revanced.patches.youtube.misc.playservice.versionCheckPatch
import app.revanced.patches.youtube.misc.settings.PreferenceScreen import app.revanced.patches.youtube.misc.settings.PreferenceScreen
import app.revanced.patches.youtube.misc.settings.settingsPatch import app.revanced.patches.youtube.misc.settings.settingsPatch
@@ -93,103 +94,109 @@ val shortsAutoplayPatch = bytecodePatch(
) )
} }
val reelPlaybackRepeatMethod = // YouTube 21.29+ replaced the diagnostics string and reworked the reel playback repeat
reelPlaybackRepeatParentMethod.immutableClassDef.getReelPlaybackRepeatMethod() // method (its signature changed from "V(L)" to "bL(JLbetu;J)V"), so the parent fingerprint
// below no longer resolves on 21.34+. The autoplay restore logic thus only applies to
reelPlaybackRepeatMethod.apply { // versions below 21.34.
// The behavior enums are looked up from an ordinal value to an enum type. if (!is_21_34_or_greater) {
findInstructionIndicesReversedOrThrow { val reelPlaybackRepeatMethod =
val reference = getReference<MethodReference>() reelPlaybackRepeatParentMethod.immutableClassDef.getReelPlaybackRepeatMethod()
reference?.definingClass == reelEnumClass &&
reference.parameterTypes.firstOrNull() == "I" &&
reference.returnType == reelEnumClass
}.forEach { index ->
val register = getInstruction<OneRegisterInstruction>(index + 1).registerA
addInstructions(
index + 2,
"""
invoke-static {v$register}, $EXTENSION_CLASS_DESCRIPTOR->changeShortsRepeatBehavior(Ljava/lang/Enum;)Ljava/lang/Enum;
move-result-object v$register
""",
)
}
}
// As of YouTube 20.09, Google has removed the code for 'Autoplay' and 'Pause' from this method.
// Manually restore the removed 'Autoplay' code.
if (is_20_09_or_greater) {
// Variable names are only a rough guess of what these methods do.
val userActionMethodReference =
reelPlaybackMethodMatch.method.getInstruction(reelPlaybackMethodMatch[1]).methodReference!!
val reelSequenceControllerMethodReference =
reelPlaybackMethodMatch.method.getInstruction(reelPlaybackMethodMatch[2]).methodReference!!
reelPlaybackRepeatMethod.apply { reelPlaybackRepeatMethod.apply {
// Find the first call modified by extension code above. // The behavior enums are looked up from an ordinal value to an enum type.
val extensionReturnResultIndex = indexOfFirstInstructionOrThrow { findInstructionIndicesReversedOrThrow {
opcode == Opcode.INVOKE_STATIC && val reference = getReference<MethodReference>()
getReference<MethodReference>()?.definingClass == EXTENSION_CLASS_DESCRIPTOR reference?.definingClass == reelEnumClass &&
} + 1 reference.parameterTypes.firstOrNull() == "I" &&
val enumRegister = reference.returnType == reelEnumClass
getInstruction<OneRegisterInstruction>(extensionReturnResultIndex).registerA }.forEach { index ->
val getReelSequenceControllerIndex = indexOfFirstInstructionOrThrow { val register = getInstruction<OneRegisterInstruction>(index + 1).registerA
val reference = getReference<FieldReference>()
opcode == Opcode.IGET_OBJECT &&
reference?.definingClass == definingClass &&
reference.type == reelSequenceControllerMethodReference.definingClass
}
val getReelSequenceControllerReference =
getInstruction<ReferenceInstruction>(getReelSequenceControllerIndex).reference
// Add a helper method to avoid finding multiple free registers. addInstructions(
// If enum is autoplay then method performs autoplay and returns null, index + 2,
// otherwise returns the same enum.
val helperClass = definingClass
val helperName = "patch_handleAutoPlay"
val helperReturnType = "Ljava/lang/Enum;"
val helperMethod = ImmutableMethod(
helperClass,
helperName,
listOf(ImmutableMethodParameter("Ljava/lang/Enum;", null, null)),
helperReturnType,
AccessFlags.PRIVATE.value,
null,
null,
MutableMethodImplementation(7),
).toMutable().apply {
addInstructionsWithLabels(
0,
""" """
invoke-static { p1 }, $EXTENSION_CLASS_DESCRIPTOR->isAutoPlay(Ljava/lang/Enum;)Z invoke-static {v$register}, $EXTENSION_CLASS_DESCRIPTOR->changeShortsRepeatBehavior(Ljava/lang/Enum;)Ljava/lang/Enum;
move-result v0 move-result-object v$register
if-eqz v0, :ignore
new-instance v0, ${userActionMethodReference.definingClass}
const/4 v1, 0x3
const/4 v2, 0x0
invoke-direct { v0, v1, v2, v2 }, $userActionMethodReference
iget-object v3, p0, $getReelSequenceControllerReference
invoke-virtual { v3, v0 }, $reelSequenceControllerMethodReference
const/4 v4, 0x0
return-object v4
:ignore
return-object p1
""", """,
) )
} }
reelPlaybackRepeatMethod.classDef.methods.add(helperMethod) }
addInstructionsWithLabels( // As of YouTube 20.09, Google has removed the code for 'Autoplay' and 'Pause' from this method.
extensionReturnResultIndex + 1, // Manually restore the removed 'Autoplay' code.
""" if (is_20_09_or_greater) {
invoke-direct { p0, v$enumRegister }, $helperClass->$helperName(Ljava/lang/Enum;)$helperReturnType // Variable names are only a rough guess of what these methods do.
move-result-object v$enumRegister val userActionMethodReference =
if-nez v$enumRegister, :ignore reelPlaybackMethodMatch.method.getInstruction(reelPlaybackMethodMatch[1]).methodReference!!
return-void # Autoplay was performed. val reelSequenceControllerMethodReference =
:ignore reelPlaybackMethodMatch.method.getInstruction(reelPlaybackMethodMatch[2]).methodReference!!
nop
""", reelPlaybackRepeatMethod.apply {
) // Find the first call modified by extension code above.
val extensionReturnResultIndex = indexOfFirstInstructionOrThrow {
opcode == Opcode.INVOKE_STATIC &&
getReference<MethodReference>()?.definingClass == EXTENSION_CLASS_DESCRIPTOR
} + 1
val enumRegister =
getInstruction<OneRegisterInstruction>(extensionReturnResultIndex).registerA
val getReelSequenceControllerIndex = indexOfFirstInstructionOrThrow {
val reference = getReference<FieldReference>()
opcode == Opcode.IGET_OBJECT &&
reference?.definingClass == definingClass &&
reference.type == reelSequenceControllerMethodReference.definingClass
}
val getReelSequenceControllerReference =
getInstruction<ReferenceInstruction>(getReelSequenceControllerIndex).reference
// Add a helper method to avoid finding multiple free registers.
// If enum is autoplay then method performs autoplay and returns null,
// otherwise returns the same enum.
val helperClass = definingClass
val helperName = "patch_handleAutoPlay"
val helperReturnType = "Ljava/lang/Enum;"
val helperMethod = ImmutableMethod(
helperClass,
helperName,
listOf(ImmutableMethodParameter("Ljava/lang/Enum;", null, null)),
helperReturnType,
AccessFlags.PRIVATE.value,
null,
null,
MutableMethodImplementation(7),
).toMutable().apply {
addInstructionsWithLabels(
0,
"""
invoke-static { p1 }, $EXTENSION_CLASS_DESCRIPTOR->isAutoPlay(Ljava/lang/Enum;)Z
move-result v0
if-eqz v0, :ignore
new-instance v0, ${userActionMethodReference.definingClass}
const/4 v1, 0x3
const/4 v2, 0x0
invoke-direct { v0, v1, v2, v2 }, $userActionMethodReference
iget-object v3, p0, $getReelSequenceControllerReference
invoke-virtual { v3, v0 }, $reelSequenceControllerMethodReference
const/4 v4, 0x0
return-object v4
:ignore
return-object p1
""",
)
}
reelPlaybackRepeatMethod.classDef.methods.add(helperMethod)
addInstructionsWithLabels(
extensionReturnResultIndex + 1,
"""
invoke-direct { p0, v$enumRegister }, $helperClass->$helperName(Ljava/lang/Enum;)$helperReturnType
move-result-object v$enumRegister
if-nez v$enumRegister, :ignore
return-void # Autoplay was performed.
:ignore
nop
""",
)
}
} }
} }
} }
@@ -36,7 +36,8 @@ val changeStartPagePatch = bytecodePatch(
"20.26.46", "20.26.46",
"20.31.42", "20.31.42",
"20.37.48", "20.37.48",
"20.40.45" "20.40.45",
"21.34.248"
), ),
) )
@@ -29,7 +29,8 @@ val announcementsPatch = bytecodePatch(
"20.26.46", "20.26.46",
"20.31.42", "20.31.42",
"20.37.48", "20.37.48",
"20.40.45" "20.40.45",
"21.34.248"
), ),
) )
@@ -12,6 +12,7 @@ import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch
import app.revanced.patches.youtube.misc.playertype.playerTypeHookPatch import app.revanced.patches.youtube.misc.playertype.playerTypeHookPatch
import app.revanced.patches.youtube.misc.playservice.is_19_34_or_greater import app.revanced.patches.youtube.misc.playservice.is_19_34_or_greater
import app.revanced.patches.youtube.misc.playservice.is_20_29_or_greater import app.revanced.patches.youtube.misc.playservice.is_20_29_or_greater
import app.revanced.patches.youtube.misc.playservice.is_21_34_or_greater
import app.revanced.patches.youtube.misc.playservice.versionCheckPatch import app.revanced.patches.youtube.misc.playservice.versionCheckPatch
import app.revanced.patches.youtube.misc.settings.PreferenceScreen import app.revanced.patches.youtube.misc.settings.PreferenceScreen
import app.revanced.patches.youtube.misc.settings.settingsPatch import app.revanced.patches.youtube.misc.settings.settingsPatch
@@ -103,7 +104,10 @@ val removeBackgroundPlaybackRestrictionsPatch = bytecodePatch(
kidsBackgroundPlaybackPolicyControllerMethod.returnEarly() kidsBackgroundPlaybackPolicyControllerMethod.returnEarly()
// Fix PiP buttons not working after locking/unlocking device screen. // Fix PiP buttons not working after locking/unlocking device screen.
if (is_19_34_or_greater) { // YouTube 21.34+ removed the flag this anchors on (the remaining case is an
// unfixable Google-side issue, see RVX #2764). Skipping it avoids the
// fingerprint being unresolved and crashing the whole patch.
if (is_19_34_or_greater && !is_21_34_or_greater) {
pipInputConsumerFeatureFlagMethodMatch.let { pipInputConsumerFeatureFlagMethodMatch.let {
it.method.insertLiteralOverride( it.method.insertLiteralOverride(
it[0], it[0],
@@ -15,6 +15,7 @@ import app.revanced.patcher.patch.BytecodePatchContext
import app.revanced.patcher.patch.bytecodePatch import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patcher.returnType import app.revanced.patcher.returnType
import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch
import app.revanced.patches.youtube.misc.playservice.is_21_34_or_greater
import app.revanced.util.addInstructionsAtControlFlowLabel import app.revanced.util.addInstructionsAtControlFlowLabel
import app.revanced.util.cloneMutableAndPreserveParameters import app.revanced.util.cloneMutableAndPreserveParameters
import app.revanced.util.findInstructionIndicesReversedOrThrow import app.revanced.util.findInstructionIndicesReversedOrThrow
@@ -73,12 +74,25 @@ val hookClientContextPatch = bytecodePatch(
} }
} }
authenticationChangeListenerMethod.apply { // The Authentication changed listener method (where the message lite builder method was
val messageLiteBuilderIndex = // sourced from) was removed in YouTube 21.34+. Derive the builder method from the dummy
indexOfMessageLiteBuilderReference(this, messageLiteBuilderField.definingClass) // client context body method instead, which still resolves and builds the same body.
if (!is_21_34_or_greater) {
authenticationChangeListenerMethod.apply {
val messageLiteBuilderIndex =
indexOfMessageLiteBuilderReference(this, messageLiteBuilderField.definingClass)
messageLiteBuilderMethod = messageLiteBuilderMethod =
getInstruction<ReferenceInstruction>(messageLiteBuilderIndex).methodReference!! getInstruction<ReferenceInstruction>(messageLiteBuilderIndex).methodReference!!
}
} else {
buildDummyClientContextBodyMethodMatch.method.apply {
val messageLiteBuilderIndex =
indexOfMessageLiteBuilderReference(this, messageLiteBuilderField.definingClass)
messageLiteBuilderMethod =
getInstruction<ReferenceInstruction>(messageLiteBuilderIndex).methodReference!!
}
} }
buildClientContextBodyConstructorMethod.immutableClassDef.buildClientContextBodyMethodMatch.let { buildClientContextBodyConstructorMethod.immutableClassDef.buildClientContextBodyMethodMatch.let {
@@ -24,7 +24,8 @@ val enableDebuggingPatch = enableDebuggingPatch(
"20.26.46", "20.26.46",
"20.31.42", "20.31.42",
"20.37.48", "20.37.48",
"20.40.45" "20.40.45",
"21.34.248"
) )
) )
}, },
@@ -11,7 +11,6 @@ internal val BytecodePatchContext.specificNetworkErrorViewControllerMethodMatch
returnType("V") returnType("V")
parameterTypes() parameterTypes()
instructions( instructions(
ResourceType.DRAWABLE("ic_offline_no_content_upside_down"),
ResourceType.STRING("offline_no_content_body_text_not_offline_eligible"), ResourceType.STRING("offline_no_content_body_text_not_offline_eligible"),
method { name == "getString" && returnType == "Ljava/lang/String;" }, method { name == "getString" && returnType == "Ljava/lang/String;" },
after(Opcode.MOVE_RESULT_OBJECT()), after(Opcode.MOVE_RESULT_OBJECT()),
@@ -20,12 +19,13 @@ internal val BytecodePatchContext.specificNetworkErrorViewControllerMethodMatch
// It's not clear if this second class is ever used, and it may be dead code, // It's not clear if this second class is ever used, and it may be dead code,
// but it is the layout image/text is identical to the network error match above. // but it is the layout image/text is identical to the network error match above.
// The offline image drawable (ic_offline_no_content_upside_down) was removed in 21.34,
// so only the still-present offline_no_content string is matched.
internal val BytecodePatchContext.loadingFrameLayoutControllerMethodMatch by composingFirstMethod { internal val BytecodePatchContext.loadingFrameLayoutControllerMethodMatch by composingFirstMethod {
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
returnType("V") returnType("V")
parameterTypes("L") parameterTypes("Z")
instructions( instructions(
ResourceType.DRAWABLE("ic_offline_no_content_upside_down"),
ResourceType.STRING("offline_no_content_body_text_not_offline_eligible"), ResourceType.STRING("offline_no_content_body_text_not_offline_eligible"),
method { name == "getString" && returnType == "Ljava/lang/String;" }, method { name == "getString" && returnType == "Ljava/lang/String;" },
after(Opcode.MOVE_RESULT_OBJECT()), after(Opcode.MOVE_RESULT_OBJECT()),
@@ -35,7 +35,8 @@ val bypassURLRedirectsPatch = bytecodePatch(
"20.26.46", "20.26.46",
"20.31.42", "20.31.42",
"20.37.48", "20.37.48",
"20.40.45" "20.40.45",
"21.34.248"
), ),
) )
@@ -47,7 +47,8 @@ val openLinksExternallyPatch = bytecodePatch(
"20.26.46", "20.26.46",
"20.31.42", "20.31.42",
"20.37.48", "20.37.48",
"20.40.45" "20.40.45",
"21.34.248"
), ),
) )
@@ -59,13 +59,18 @@ val lithoFilterPatch = lithoFilterPatch(
} }
// Turn off a feature flag that enables native code of protobuf parsing (Upb protobuf). // Turn off a feature flag that enables native code of protobuf parsing (Upb protobuf).
lithoConverterBufferUpbFeatureFlagMethodMatch.let { // The flag (45419603) was removed in 21.34. In 21.34 the native UPB buffer path is the
// 20.22 the flag is still enabled in one location, but what it does is not known. // default (and required) path for the setProtoBuffer([B) hook, so nothing needs to be
// Disable it anyway. // forced off.
it.method.insertLiteralOverride( if (!is_21_34_or_greater) {
it[0], lithoConverterBufferUpbFeatureFlagMethodMatch.let {
false, // 20.22 the flag is still enabled in one location, but what it does is not known.
) // Disable it anyway.
it.method.insertLiteralOverride(
it[0],
false,
)
}
} }
// endregion // endregion
} }
@@ -34,6 +34,27 @@ internal val ClassDef.motionEventMethodMatch by ClassDefComposing.composingFirst
instructions(method("setTranslationY")) instructions(method("setTranslationY"))
} }
// The player controls overlay was rebuilt in 21.34 and no longer calls `setTranslationY`.
// Instead the tap-to-dismiss handler is `nwo.b(Landroid/view/MotionEvent;)V`, which triggers the
// visibility change via `nwo.U()V`. These two fingerprints resolve that new hook and keep the
// `visibilityNegatedImmediate` injection working for the newer layout.
internal val BytecodePatchContext.youtubeControlsOverlayMethod21_34 by gettingFirstImmutableMethodDeclaratively {
returnType("V")
parameterTypes("Landroid/view/MotionEvent;")
instructions(
field { definingClass == "Lnwo;" && name == "br" },
method { definingClass == "Lnwo;" && name == "U" },
)
}
internal val ClassDef.motionEventMethodMatch21_34 by ClassDefComposing.composingFirstMethod {
returnType("V")
parameterTypes("Landroid/view/MotionEvent;")
instructions(
method { definingClass == "Lnwo;" && name == "U" },
)
}
internal val BytecodePatchContext.playerControlsExtensionHookListenersExistMethod by gettingFirstMethodDeclaratively { internal val BytecodePatchContext.playerControlsExtensionHookListenersExistMethod by gettingFirstMethodDeclaratively {
name("fullscreenButtonVisibilityCallbacksExist") name("fullscreenButtonVisibilityCallbacksExist")
definingClass(EXTENSION_CLASS_DESCRIPTOR) definingClass(EXTENSION_CLASS_DESCRIPTOR)
@@ -268,7 +268,13 @@ val playerControlsPatch = bytecodePatch(
visibilityImmediateCallbacksExistMethod = playerControlsExtensionHookListenersExistMethod visibilityImmediateCallbacksExistMethod = playerControlsExtensionHookListenersExistMethod
visibilityImmediateMethod = playerControlsExtensionHookMethod visibilityImmediateMethod = playerControlsExtensionHookMethod
youtubeControlsOverlayMethod.immutableClassDef.motionEventMethodMatch.let { val motionEventMethodMatch = if (is_21_34_or_greater) {
youtubeControlsOverlayMethod21_34.immutableClassDef.motionEventMethodMatch21_34
} else {
youtubeControlsOverlayMethod.immutableClassDef.motionEventMethodMatch
}
motionEventMethodMatch.let {
visibilityNegatedImmediateMethod = it.method visibilityNegatedImmediateMethod = it.method
visibilityNegatedImmediateInsertIndex = it[0] + 1 visibilityNegatedImmediateInsertIndex = it[0] + 1
} }
@@ -127,6 +127,8 @@ var is_21_07_or_greater : Boolean by Delegates.notNull()
private set private set
var is_21_08_or_greater : Boolean by Delegates.notNull() var is_21_08_or_greater : Boolean by Delegates.notNull()
private set private set
var is_21_34_or_greater: Boolean by Delegates.notNull()
private set
val versionCheckPatch = resourcePatch( val versionCheckPatch = resourcePatch(
description = "Uses the Play Store service version to find the major/minor version of the YouTube target app.", description = "Uses the Play Store service version to find the major/minor version of the YouTube target app.",
@@ -184,5 +186,6 @@ val versionCheckPatch = resourcePatch(
is_21_06_or_greater = 260705000 <= playStoreServicesVersion is_21_06_or_greater = 260705000 <= playStoreServicesVersion
is_21_07_or_greater = 260805000 <= playStoreServicesVersion is_21_07_or_greater = 260805000 <= playStoreServicesVersion
is_21_08_or_greater = 260905000 <= playStoreServicesVersion is_21_08_or_greater = 260905000 <= playStoreServicesVersion
is_21_34_or_greater = 263410000 <= playStoreServicesVersion
} }
} }
@@ -20,7 +20,8 @@ val sanitizeSharingLinksPatch = sanitizeSharingLinksPatch(
"20.26.46", "20.26.46",
"20.31.42", "20.31.42",
"20.37.48", "20.37.48",
"20.40.45" "20.40.45",
"21.34.248"
) )
) )
}, },
@@ -21,6 +21,27 @@ internal val BytecodePatchContext.setThemeMethod by gettingFirstMethodDeclarativ
instructions(ResourceType.STRING("app_theme_appearance_dark")) instructions(ResourceType.STRING("app_theme_appearance_dark"))
} }
/**
* YouTube 21.34+ appearance resolver `Latyq;->t()Lkll;`.
*
* Returns the theme appearance enum (whose ordinal 1 is dark), resolving between the
* `app_theme_appearance_*` prefs, the dark-mode flag and the system UI mode. Among all
* `public final ()Lkll` methods it is uniquely identified by reading `Lklm;->f` (the stored
* appearance value string): the sibling helpers `u()`/`v()` read `Lklm;->e`/the system UI mode
* instead, while `qyn` reads `Lklm;->f` but returns `V`.
*/
internal val BytecodePatchContext.setThemeMethod21_34 by gettingFirstMethodDeclaratively {
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
returnType("Lkll;")
parameterTypes()
instructions(
allOf(
Opcode.IGET_OBJECT(),
field { name == "f" && type == "Ljava/lang/String;" && definingClass == "Lklm;" },
),
)
}
internal val BytecodePatchContext.cairoFragmentConfigMethodMatch by composingFirstMethod { internal val BytecodePatchContext.cairoFragmentConfigMethodMatch by composingFirstMethod {
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
returnType("Z") returnType("Z")
@@ -29,3 +50,22 @@ internal val BytecodePatchContext.cairoFragmentConfigMethodMatch by composingFir
afterAtMost(10, Opcode.MOVE_RESULT()), afterAtMost(10, Opcode.MOVE_RESULT()),
) )
} }
/**
* YouTube 21.34+ Cairo settings gate `Loqy;->run()V`.
*
* The legacy `public final ()Z` feature-flag gate no longer exists. In 21.34 the Cairo settings
* screen is selected by feature flag `0x2b6c3c4` inside `Loqy;->run()V`, which chooses between the
* legacy (`xml/settings_fragment`, 0x7f180027) and Cairo (`xml/settings_fragment_cairo`,
* 0x7f180028) settings screens. The `0x7f180028` literal only appears in this method, which keeps
* it unambiguous among the other `0x2b6c3c4` references.
*/
internal val BytecodePatchContext.cairoFragmentConfigMethodMatch21_34 by composingFirstMethod {
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
returnType("V")
instructions(
45532100L(),
afterAtMost(10, Opcode.MOVE_RESULT()),
afterAtMost(10, 2132279336L()),
)
}
@@ -23,6 +23,7 @@ import app.revanced.patches.youtube.misc.fix.contentprovider.fixContentProviderP
import app.revanced.patches.youtube.misc.fix.playbackspeed.fixPlaybackSpeedWhilePlayingPatch import app.revanced.patches.youtube.misc.fix.playbackspeed.fixPlaybackSpeedWhilePlayingPatch
import app.revanced.patches.youtube.misc.playservice.is_19_34_or_greater import app.revanced.patches.youtube.misc.playservice.is_19_34_or_greater
import app.revanced.patches.youtube.misc.playservice.is_20_31_or_greater import app.revanced.patches.youtube.misc.playservice.is_20_31_or_greater
import app.revanced.patches.youtube.misc.playservice.is_21_34_or_greater
import app.revanced.patches.youtube.misc.playservice.versionCheckPatch import app.revanced.patches.youtube.misc.playservice.versionCheckPatch
import app.revanced.util.* import app.revanced.util.*
import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.AccessFlags
@@ -214,6 +215,74 @@ val settingsPatch = bytecodePatch(
) )
PreferenceScreen.MISC.addPreferences( PreferenceScreen.MISC.addPreferences(
PreferenceCategory(
titleKey = "revanced_custom_title",
sorting = Sorting.UNSORTED,
tag = "app.revanced.extension.shared.settings.preference.NoTitlePreferenceCategory",
preferences = setOf(
NonInteractivePreference(
key = "revanced_custom_reset_title",
titleKey = "revanced_custom_reset_title",
summaryKey = "revanced_custom_reset_summary",
tag = "app.revanced.extension.shared.settings.preference.CustomSettingsResetPreference",
),
SwitchPreference(
"revanced_custom_force_amoled",
),
SwitchPreference(
"revanced_custom_pure_black_player",
),
SwitchPreference(
"revanced_custom_disable_ambient_mode",
),
SwitchPreference(
"revanced_custom_hide_shorts",
),
// TEMPORARILY DISABLED FOR TESTING (revanced-cli verification pass): this
// ListPreference references "${key}_entries"/"${key}_entry_values" array
// resources that don't exist anywhere and can't be added via addresources/
// values/strings.xml, since patches/strings-processing.gradle.kts only
// extracts <string> elements (see its `"string" -> transform(...)` case
// vs. the `else -> walk children` fallthrough that silently drops <array>/
// <string-array>). Needs a real app.revanced.util.resource.ArrayResource
// wired through the ListPreference(entries=, entryValues=) constructor
// (see patches/.../shared/misc/settings/preference/ListPreference.kt) or
// another array-resource source before this can ship.
// ListPreference(
// key = "revanced_custom_default_playback_speed",
// tag = "app.revanced.extension.shared.settings.preference.SortedListPreference",
// ),
SwitchPreference(
"revanced_custom_remember_playback_speed",
),
// ListPreference(
// key = "revanced_custom_default_video_quality",
// tag = "app.revanced.extension.shared.settings.preference.SortedListPreference",
// ),
SwitchPreference(
"revanced_custom_remember_video_quality",
),
SwitchPreference(
"revanced_custom_force_highest_quality",
),
SwitchPreference(
"revanced_custom_debug_video_info",
),
TextPreference(
key = "revanced_custom_app_name",
titleKey = "revanced_custom_app_name_title",
inputType = InputType.TEXT_CAP_CHARACTERS,
tag = "app.revanced.extension.shared.settings.preference.TextInputPreference",
),
// ListPreference(
// key = "revanced_custom_startup_tab",
// tag = "app.revanced.extension.shared.settings.preference.SortedListPreference",
// ),
SwitchPreference(
"revanced_custom_clean_urls",
),
),
),
TextPreference( TextPreference(
key = null, key = null,
titleKey = "revanced_pref_import_export_title", titleKey = "revanced_pref_import_export_title",
@@ -230,7 +299,7 @@ val settingsPatch = bytecodePatch(
// Update shared dark mode status based on YT theme. // Update shared dark mode status based on YT theme.
// This is needed because YT allows forcing light/dark mode // This is needed because YT allows forcing light/dark mode
// which then differs from the system dark mode status. // which then differs from the system dark mode status.
setThemeMethod.apply { (if (is_21_34_or_greater) setThemeMethod21_34 else setThemeMethod).apply {
findInstructionIndicesReversedOrThrow(Opcode.RETURN_OBJECT).forEach { index -> findInstructionIndicesReversedOrThrow(Opcode.RETURN_OBJECT).forEach { index ->
val register = getInstruction<OneRegisterInstruction>(index).registerA val register = getInstruction<OneRegisterInstruction>(index).registerA
addInstructionsAtControlFlowLabel( addInstructionsAtControlFlowLabel(
@@ -241,10 +310,12 @@ val settingsPatch = bytecodePatch(
} }
// Add setting to force Cairo settings fragment on/off. // Add setting to force Cairo settings fragment on/off.
cairoFragmentConfigMethodMatch.method.insertLiteralOverride( (if (is_21_34_or_greater) cairoFragmentConfigMethodMatch21_34 else cairoFragmentConfigMethodMatch).let {
cairoFragmentConfigMethodMatch[0], it.method.insertLiteralOverride(
"$YOUTUBE_ACTIVITY_HOOK_CLASS_DESCRIPTOR->useCairoSettingsFragment(Z)Z", it[0],
) "$YOUTUBE_ACTIVITY_HOOK_CLASS_DESCRIPTOR->useCairoSettingsFragment(Z)Z",
)
}
// Bold icon resources are found starting in 20.23, but many YT icons are not bold. // Bold icon resources are found starting in 20.23, but many YT icons are not bold.
// 20.31 is the first version that seems to have all the bold icons. // 20.31 is the first version that seems to have all the bold icons.
@@ -183,6 +183,32 @@ internal val BytecodePatchContext.playbackSpeedClassMethod by gettingFirstMethod
opcodes(Opcode.RETURN_OBJECT) opcodes(Opcode.RETURN_OBJECT)
} }
/**
* YouTube 21.34+ playback-speed bottom sheet fragment factory `Lpzy;->aW(Lce;Ljava/lang/String;)Lpzy;`.
*
* In 21.34 the `PLAYBACK_RATE_MENU_BOTTOM_SHEET_FRAGMENT` string moved into the large state-holder
* factory `Lhxz;->p()Ljava/lang/Object;` (which returns `Ljava/lang/Object;`, not the fragment type),
* so the legacy `playbackSpeedClassMethod` fingerprint no longer resolves. The fragment is instead
* obtained from its static factory, lookup by tag with fallback creation, which returns the exact
* fragment instance (`Lpzy;`). It is uniquely identified by the exact return type `Lpzy;` together
* with the `getSupportFragmentManager` -> `f(Ljava/lang/String;)L` lookup sequence.
*/
internal val BytecodePatchContext.playbackSpeedClassMethod21_34 by gettingFirstMethodDeclaratively {
accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC)
returnType("Lpzy;")
parameterTypes("L", "Ljava/lang/String;")
instructions(
method("getSupportFragmentManager"),
after(Opcode.MOVE_RESULT_OBJECT()),
after(method {
returnType.startsWith("L") && parameterTypes.size == 1 && parameterTypes.first() == "Ljava/lang/String;"
}),
after(Opcode.MOVE_RESULT_OBJECT()),
after(Opcode.IF_EQZ()),
after(Opcode.CHECK_CAST()),
)
}
/** /**
* YouTube 20.19 and lower. * YouTube 20.19 and lower.
*/ */
@@ -12,6 +12,7 @@ import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch
import app.revanced.patches.youtube.misc.playservice.is_20_19_or_greater import app.revanced.patches.youtube.misc.playservice.is_20_19_or_greater
import app.revanced.patches.youtube.misc.playservice.is_20_20_or_greater import app.revanced.patches.youtube.misc.playservice.is_20_20_or_greater
import app.revanced.patches.youtube.misc.playservice.is_20_49_or_greater import app.revanced.patches.youtube.misc.playservice.is_20_49_or_greater
import app.revanced.patches.youtube.misc.playservice.is_21_34_or_greater
import app.revanced.patches.youtube.misc.playservice.versionCheckPatch import app.revanced.patches.youtube.misc.playservice.versionCheckPatch
import app.revanced.patches.youtube.shared.videoQualityChangedMethodMatch import app.revanced.patches.youtube.shared.videoQualityChangedMethodMatch
import app.revanced.patches.youtube.video.playerresponse.Hook import app.revanced.patches.youtube.video.playerresponse.Hook
@@ -283,7 +284,7 @@ val videoInformationPatch = bytecodePatch(
) )
} }
playbackSpeedClassMethod.apply { (if (is_21_34_or_greater) playbackSpeedClassMethod21_34 else playbackSpeedClassMethod).apply {
val index = indexOfFirstInstructionOrThrow(Opcode.RETURN_OBJECT) val index = indexOfFirstInstructionOrThrow(Opcode.RETURN_OBJECT)
val register = getInstruction<OneRegisterInstruction>(index).registerA val register = getInstruction<OneRegisterInstruction>(index).registerA
val playbackSpeedClass = this.returnType val playbackSpeedClass = this.returnType
@@ -241,6 +241,42 @@ However, enabling this will also log some user data such as your IP address."</s
<string name="revanced_restore_old_settings_menus_title">Restore old settings menus</string> <string name="revanced_restore_old_settings_menus_title">Restore old settings menus</string>
<string name="revanced_restore_old_settings_menus_summary_on">Old settings menus are shown</string> <string name="revanced_restore_old_settings_menus_summary_on">Old settings menus are shown</string>
<string name="revanced_restore_old_settings_menus_summary_off">Old settings menus are not shown</string> <string name="revanced_restore_old_settings_menus_summary_off">Old settings menus are not shown</string>
<!-- Custom settings -->
<string name="revanced_custom_title">Custom</string>
<string name="revanced_custom_reset_title">Reset Custom Settings</string>
<string name="revanced_custom_reset_summary">Restore custom preferences to defaults</string>
<string name="revanced_custom_force_amoled_title">Force AMOLED</string>
<string name="revanced_custom_force_amoled_summary_on">True black is used in the YouTube UI</string>
<string name="revanced_custom_force_amoled_summary_off">True black is not used in the YouTube UI</string>
<string name="revanced_custom_pure_black_player_title">Pure Black Player</string>
<string name="revanced_custom_pure_black_player_summary_on">Player background is pure black</string>
<string name="revanced_custom_pure_black_player_summary_off">Player background is not pure black</string>
<string name="revanced_custom_disable_ambient_mode_title">Disable Ambient Mode</string>
<string name="revanced_custom_disable_ambient_mode_summary_on">Ambient Mode is prevented from activating</string>
<string name="revanced_custom_disable_ambient_mode_summary_off">Ambient Mode can activate</string>
<string name="revanced_custom_hide_shorts_title">Hide Shorts</string>
<string name="revanced_custom_hide_shorts_summary_on">Shorts UI entries are removed</string>
<string name="revanced_custom_hide_shorts_summary_off">Shorts UI entries are shown</string>
<string name="revanced_custom_default_playback_speed_title">Default Playback Speed</string>
<string name="revanced_custom_remember_playback_speed_title">Remember Playback Speed</string>
<string name="revanced_custom_remember_playback_speed_summary_on">Playback speed is remembered across videos</string>
<string name="revanced_custom_remember_playback_speed_summary_off">Playback speed is not remembered across videos</string>
<string name="revanced_custom_default_video_quality_title">Default Video Quality</string>
<string name="revanced_custom_remember_video_quality_title">Remember Video Quality</string>
<string name="revanced_custom_remember_video_quality_summary_on">Video quality is remembered across videos</string>
<string name="revanced_custom_remember_video_quality_summary_off">Video quality is not remembered across videos</string>
<string name="revanced_custom_force_highest_quality_title">Force Highest Available Quality</string>
<string name="revanced_custom_force_highest_quality_summary_on">Highest quality option is preferred</string>
<string name="revanced_custom_force_highest_quality_summary_off">Highest quality option is not preferred</string>
<string name="revanced_custom_debug_video_info_title">Debug Video Information</string>
<string name="revanced_custom_debug_video_info_summary_on">Technical video details are shown</string>
<string name="revanced_custom_debug_video_info_summary_off">Technical video details are hidden</string>
<string name="revanced_custom_app_name_title">Custom App Name</string>
<string name="revanced_custom_app_name_summary">Change the visible application name</string>
<string name="revanced_custom_startup_tab_title">Default Startup Tab</string>
<string name="revanced_custom_clean_urls_title">Clean YouTube URLs</string>
<string name="revanced_custom_clean_urls_summary_on">Tracking parameters are removed from shared links</string>
<string name="revanced_custom_clean_urls_summary_off">Tracking parameters are not removed from shared links</string>
</patch> </patch>
<patch id="misc.backgroundplayback.backgroundPlaybackPatch"> <patch id="misc.backgroundplayback.backgroundPlaybackPatch">
<string name="revanced_shorts_disable_background_playback_title">Disable Shorts background play</string> <string name="revanced_shorts_disable_background_playback_title">Disable Shorts background play</string>
@@ -1516,6 +1552,7 @@ Limitation: Using the back button on the toolbar may not work"</string>
<string name="revanced_miniplayer_type_entry_5">Modern 2</string> <string name="revanced_miniplayer_type_entry_5">Modern 2</string>
<string name="revanced_miniplayer_type_entry_6">Modern 3</string> <string name="revanced_miniplayer_type_entry_6">Modern 3</string>
<string name="revanced_miniplayer_type_entry_7">Modern 4</string> <string name="revanced_miniplayer_type_entry_7">Modern 4</string>
<string name="revanced_miniplayer_type_not_available_summary">Selecting a miniplayer type is no longer available on your YouTube version.</string>
<string name="revanced_miniplayer_disable_rounded_corners_title">Disable rounded corners</string> <string name="revanced_miniplayer_disable_rounded_corners_title">Disable rounded corners</string>
<string name="revanced_miniplayer_disable_rounded_corners_summary_on">Corners are square</string> <string name="revanced_miniplayer_disable_rounded_corners_summary_on">Corners are square</string>
<string name="revanced_miniplayer_disable_rounded_corners_summary_off">Corners are rounded</string> <string name="revanced_miniplayer_disable_rounded_corners_summary_off">Corners are rounded</string>