From dc69f2433e2650654e2dffdd76b0b0c8a52bf515 Mon Sep 17 00:00:00 2001
From: ILoveOpenSourceApplications
<117499019+ILoveOpenSourceApplications@users.noreply.github.com>
Date: Mon, 22 Dec 2025 03:40:35 +0530
Subject: [PATCH 01/42] fix(YouTube - Hide layout components): Hide new type of
crowdfunding box (#6380)
---
.../patches/components/LayoutComponentsFilter.java | 8 +++++++-
.../revanced/extension/youtube/settings/Settings.java | 4 ++--
.../layout/hide/general/HideLayoutComponentsPatch.kt | 2 +-
.../src/main/resources/addresources/values/strings.xml | 10 +++++-----
4 files changed, 15 insertions(+), 9 deletions(-)
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/LayoutComponentsFilter.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/LayoutComponentsFilter.java
index 33ac4adc4..707e18bc6 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/LayoutComponentsFilter.java
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/LayoutComponentsFilter.java
@@ -247,8 +247,13 @@ public final class LayoutComponentsFilter extends Filter {
"sponsorships"
);
+ final var crowdfundingBox = new StringFilterGroup(
+ Settings.HIDE_CROWDFUNDING_BOX,
+ "donation_shelf"
+ );
+
final var channelWatermark = new StringFilterGroup(
- Settings.HIDE_VIDEO_CHANNEL_WATERMARK,
+ Settings.HIDE_CHANNEL_WATERMARK,
"featured_channel_watermark_overlay"
);
@@ -312,6 +317,7 @@ public final class LayoutComponentsFilter extends Filter {
compactChannelBar,
compactChannelBarInner,
communityPosts,
+ crowdfundingBox,
emergencyBox,
expandableMetadata,
forYouShelf,
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/Settings.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/Settings.java
index b85939e7e..62fa7bcae 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/Settings.java
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/Settings.java
@@ -96,7 +96,6 @@ public class Settings extends BaseSettings {
public static final BooleanSetting HIDE_CHIPS_SHELF = new BooleanSetting("revanced_hide_chips_shelf", TRUE);
public static final BooleanSetting HIDE_COMMUNITY_POSTS = new BooleanSetting("revanced_hide_community_posts", FALSE);
public static final BooleanSetting HIDE_COMPACT_BANNER = new BooleanSetting("revanced_hide_compact_banner", TRUE);
- public static final BooleanSetting HIDE_CROWDFUNDING_BOX = new BooleanSetting("revanced_hide_crowdfunding_box", FALSE, true);
public static final BooleanSetting HIDE_DOODLES = new BooleanSetting("revanced_hide_doodles", FALSE, true, "revanced_hide_doodles_user_dialog_message");
public static final BooleanSetting HIDE_EXPANDABLE_CARD = new BooleanSetting("revanced_hide_expandable_card", TRUE);
public static final BooleanSetting HIDE_FILTER_BAR_FEED_IN_FEED = new BooleanSetting("revanced_hide_filter_bar_feed_in_feed", FALSE, true);
@@ -158,6 +157,8 @@ public class Settings extends BaseSettings {
public static final BooleanSetting HIDE_CAPTIONS_BUTTON = new BooleanSetting("revanced_hide_captions_button", FALSE);
public static final BooleanSetting HIDE_CAST_BUTTON = new BooleanSetting("revanced_hide_cast_button", TRUE, true);
public static final BooleanSetting HIDE_CHANNEL_BAR = new BooleanSetting("revanced_hide_channel_bar", FALSE);
+ public static final BooleanSetting HIDE_CHANNEL_WATERMARK = new BooleanSetting("revanced_hide_channel_watermark", TRUE);
+ public static final BooleanSetting HIDE_CROWDFUNDING_BOX = new BooleanSetting("revanced_hide_crowdfunding_box", FALSE, true);
public static final BooleanSetting HIDE_EMERGENCY_BOX = new BooleanSetting("revanced_hide_emergency_box", TRUE);
public static final BooleanSetting HIDE_ENDSCREEN_CARDS = new BooleanSetting("revanced_hide_endscreen_cards", FALSE);
public static final BooleanSetting HIDE_END_SCREEN_SUGGESTED_VIDEO = new BooleanSetting("revanced_end_screen_suggested_video", FALSE, true);
@@ -172,7 +173,6 @@ public class Settings extends BaseSettings {
public static final BooleanSetting HIDE_RELATED_VIDEOS = new BooleanSetting("revanced_hide_related_videos", FALSE);
public static final BooleanSetting HIDE_SUBSCRIBERS_COMMUNITY_GUIDELINES = new BooleanSetting("revanced_hide_subscribers_community_guidelines", TRUE);
public static final BooleanSetting HIDE_TIMED_REACTIONS = new BooleanSetting("revanced_hide_timed_reactions", TRUE);
- public static final BooleanSetting HIDE_VIDEO_CHANNEL_WATERMARK = new BooleanSetting("revanced_hide_channel_watermark", TRUE);
public static final BooleanSetting OPEN_VIDEOS_FULLSCREEN_PORTRAIT = new BooleanSetting("revanced_open_videos_fullscreen_portrait", FALSE);
public static final BooleanSetting PLAYBACK_SPEED_DIALOG_BUTTON = new BooleanSetting("revanced_playback_speed_dialog_button", FALSE);
public static final BooleanSetting VIDEO_QUALITY_DIALOG_BUTTON = new BooleanSetting("revanced_video_quality_dialog_button", FALSE);
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/general/HideLayoutComponentsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/general/HideLayoutComponentsPatch.kt
index ae62a4914..07eecb83f 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/general/HideLayoutComponentsPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/general/HideLayoutComponentsPatch.kt
@@ -174,6 +174,7 @@ val hideLayoutComponentsPatch = bytecodePatch(
),
SwitchPreference("revanced_hide_channel_bar"),
SwitchPreference("revanced_hide_channel_watermark"),
+ SwitchPreference("revanced_hide_crowdfunding_box"),
SwitchPreference("revanced_hide_emergency_box"),
SwitchPreference("revanced_hide_info_panels"),
SwitchPreference("revanced_hide_join_membership_button"),
@@ -229,7 +230,6 @@ val hideLayoutComponentsPatch = bytecodePatch(
SwitchPreference("revanced_hide_chips_shelf"),
SwitchPreference("revanced_hide_community_posts"),
SwitchPreference("revanced_hide_compact_banner"),
- SwitchPreference("revanced_hide_crowdfunding_box"),
SwitchPreference("revanced_hide_expandable_card"),
SwitchPreference("revanced_hide_floating_microphone_button"),
SwitchPreference(
diff --git a/patches/src/main/resources/addresources/values/strings.xml b/patches/src/main/resources/addresources/values/strings.xml
index ddb6d1d70..53e99d70d 100644
--- a/patches/src/main/resources/addresources/values/strings.xml
+++ b/patches/src/main/resources/addresources/values/strings.xml
@@ -232,9 +232,6 @@ However, enabling this will also log some user data such as your IP address."Hide compact banners
Compact banners are hidden
Compact banners are shown
- Hide crowdfunding box
- Crowdfunding box is hidden
- Crowdfunding box is shown
Hide expandable card
Expandable card under videos is hidden
Expandable card under videos is shown
@@ -300,8 +297,11 @@ If a Doodle is currently showing in your region and this hide setting is on, the
Channel bar is hidden
Channel bar is shown
Hide channel watermark
- Watermark is hidden
- Watermark is shown
+ Channel watermark is hidden
+ Channel watermark is shown
+ Hide crowdfunding box
+ Crowdfunding box is hidden
+ Crowdfunding box is shown
Hide emergency boxes
Emergency boxes are hidden
Emergency boxes are shown
From 315931cbf8f61cd4b3a54ace1ff03685d748614c Mon Sep 17 00:00:00 2001
From: trespyian
Date: Tue, 23 Dec 2025 00:49:19 +1100
Subject: [PATCH 02/42] feat(SBS On Demand): Add `Remove ads` patch (#6378)
Co-authored-by: Trespyian
Co-authored-by: oSumAtrIX
---
patches/api/patches.api | 4 ++
.../com/sbs/ondemand/tv/Fingerprints.kt | 28 ++++++++++++++
.../com/sbs/ondemand/tv/RemoveAdsPatch.kt | 37 +++++++++++++++++++
3 files changed, 69 insertions(+)
create mode 100644 patches/src/main/kotlin/app/revanced/patches/com/sbs/ondemand/tv/Fingerprints.kt
create mode 100644 patches/src/main/kotlin/app/revanced/patches/com/sbs/ondemand/tv/RemoveAdsPatch.kt
diff --git a/patches/api/patches.api b/patches/api/patches.api
index fdb8c1cf2..9f779cbed 100644
--- a/patches/api/patches.api
+++ b/patches/api/patches.api
@@ -176,6 +176,10 @@ public final class app/revanced/patches/cieid/restrictions/root/BypassRootChecks
public static final fun getBypassRootChecksPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
+public final class app/revanced/patches/com/sbs/ondemand/tv/RemoveAdsPatchKt {
+ public static final fun getRemoveAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
+}
+
public final class app/revanced/patches/cricbuzz/ads/DisableAdsPatchKt {
public static final fun getDisableAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
diff --git a/patches/src/main/kotlin/app/revanced/patches/com/sbs/ondemand/tv/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/com/sbs/ondemand/tv/Fingerprints.kt
new file mode 100644
index 000000000..bf2ee4120
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/com/sbs/ondemand/tv/Fingerprints.kt
@@ -0,0 +1,28 @@
+package app.revanced.patches.com.sbs.ondemand.tv
+
+import app.revanced.patcher.fingerprint
+
+internal val shouldShowAdvertisingTVFingerprint = fingerprint {
+ returns("Z")
+ custom { method, classDef ->
+ method.name == "getShouldShowAdvertisingTV" &&
+ classDef.type == "Lcom/sbs/ondemand/common/InMemoryStorage;"
+ }
+}
+
+internal val shouldShowPauseAdFingerprint = fingerprint {
+ returns("Z")
+ custom { method, classDef ->
+ method.name == "shouldShowPauseAd" &&
+ classDef.type == "Lcom/sbs/ondemand/player/viewmodels/PauseAdController;"
+ }
+}
+
+internal val requestAdStreamFingerprint = fingerprint {
+ returns("V")
+ custom { method, classDef ->
+ method.name == "requestAdStream\$player_googleStoreTvRelease" &&
+ classDef.type == "Lcom/sbs/ondemand/player/viewmodels/AdsController;"
+ }
+}
+
diff --git a/patches/src/main/kotlin/app/revanced/patches/com/sbs/ondemand/tv/RemoveAdsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/com/sbs/ondemand/tv/RemoveAdsPatch.kt
new file mode 100644
index 000000000..1b628562a
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/com/sbs/ondemand/tv/RemoveAdsPatch.kt
@@ -0,0 +1,37 @@
+package app.revanced.patches.com.sbs.ondemand.tv
+
+import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
+import app.revanced.patcher.patch.bytecodePatch
+import app.revanced.patches.shared.misc.pairip.license.disableLicenseCheckPatch
+import app.revanced.util.returnEarly
+
+@Suppress("unused")
+val removeAdsPatch = bytecodePatch(
+ name = "Remove ads",
+ description = "Removes pre-roll, pause and on-demand advertisements from SBS On Demand TV.",
+) {
+ compatibleWith("com.sbs.ondemand.tv")
+
+ dependsOn(disableLicenseCheckPatch)
+
+ execute {
+ shouldShowAdvertisingTVFingerprint.method.returnEarly(true)
+ shouldShowPauseAdFingerprint.method.returnEarly(false)
+
+ // Remove on-demand pre-roll advertisements using exception handling.
+ // Exception handling is used instead of returnEarly() because:
+ // 1. returnEarly() causes black screen when the app waits for ad content that never comes.
+ // 2. SBS app has built-in exception handling in handleProviderFailure().
+ // 3. Exception triggers fallbackToAkamaiProvider() which loads actual content.
+ // 4. This preserves the intended app flow: first try ads, then fail gracefully, then load content.
+ requestAdStreamFingerprint.method.addInstructions(
+ 0,
+ """
+ new-instance v0, Ljava/lang/RuntimeException;
+ const-string v1, "Ad stream disabled"
+ invoke-direct {v0, v1}, Ljava/lang/RuntimeException;->(Ljava/lang/String;)V
+ throw v0
+ """
+ )
+ }
+}
From e0f33468e6e96b9f10cf35ec67622d6488528c90 Mon Sep 17 00:00:00 2001
From: Sylvain Finot <25269226+SF73@users.noreply.github.com>
Date: Mon, 22 Dec 2025 15:00:08 +0100
Subject: [PATCH 03/42] feat(ProtonVPN): Add `Unlock split tunneling` patch
(#6353)
Co-authored-by: oSumAtrIX
---
patches/api/patches.api | 4 +++
.../protonvpn/splittunneling/Fingerprints.kt | 20 +++++++++++
.../splittunneling/UnlockSplitTunneling.kt | 34 +++++++++++++++++++
3 files changed, 58 insertions(+)
create mode 100644 patches/src/main/kotlin/app/revanced/patches/protonvpn/splittunneling/Fingerprints.kt
create mode 100644 patches/src/main/kotlin/app/revanced/patches/protonvpn/splittunneling/UnlockSplitTunneling.kt
diff --git a/patches/api/patches.api b/patches/api/patches.api
index 9f779cbed..c8438af90 100644
--- a/patches/api/patches.api
+++ b/patches/api/patches.api
@@ -649,6 +649,10 @@ public final class app/revanced/patches/protonvpn/delay/RemoveDelayPatchKt {
public static final fun getRemoveDelayPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
+public final class app/revanced/patches/protonvpn/splittunneling/UnlockSplitTunnelingKt {
+ public static final fun getUnlockSplitTunnelingPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
+}
+
public final class app/revanced/patches/rar/misc/annoyances/purchasereminder/HidePurchaseReminderPatchKt {
public static final fun getHidePurchaseReminderPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
diff --git a/patches/src/main/kotlin/app/revanced/patches/protonvpn/splittunneling/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/protonvpn/splittunneling/Fingerprints.kt
new file mode 100644
index 000000000..639141723
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/protonvpn/splittunneling/Fingerprints.kt
@@ -0,0 +1,20 @@
+package app.revanced.patches.protonvpn.splittunneling
+
+import app.revanced.patcher.fingerprint
+import com.android.tools.smali.dexlib2.AccessFlags
+import com.android.tools.smali.dexlib2.Opcode
+
+internal val enableSplitTunnelingUiFingerprint = fingerprint {
+ strings("currentModeAppNames")
+ opcodes(
+ Opcode.MOVE_OBJECT,
+ Opcode.MOVE_FROM16,
+ Opcode.INVOKE_DIRECT_RANGE
+ )
+}
+
+internal val initializeSplitTunnelingSettingsUIFingerprint = fingerprint {
+ custom { method, _ ->
+ method.name == "applyRestrictions"
+ }
+}
\ No newline at end of file
diff --git a/patches/src/main/kotlin/app/revanced/patches/protonvpn/splittunneling/UnlockSplitTunneling.kt b/patches/src/main/kotlin/app/revanced/patches/protonvpn/splittunneling/UnlockSplitTunneling.kt
new file mode 100644
index 000000000..f50a5f993
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/protonvpn/splittunneling/UnlockSplitTunneling.kt
@@ -0,0 +1,34 @@
+package app.revanced.patches.protonvpn.splittunneling
+
+import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
+import app.revanced.patcher.extensions.InstructionExtensions.removeInstruction
+import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
+import app.revanced.patcher.patch.bytecodePatch
+import app.revanced.util.getReference
+import app.revanced.util.indexOfFirstInstructionOrThrow
+import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
+import com.android.tools.smali.dexlib2.iface.reference.MethodReference
+
+@Suppress("unused")
+val unlockSplitTunnelingPatch =
+ bytecodePatch(
+ name = "Unlock split tunneling",
+ ) {
+ compatibleWith("ch.protonvpn.android")
+
+ execute {
+ val registerIndex = enableSplitTunnelingUiFingerprint.patternMatch!!.endIndex - 1
+
+ enableSplitTunnelingUiFingerprint.method.apply {
+ val register = getInstruction(registerIndex).registerA
+ replaceInstruction(registerIndex, "const/4 v$register, 0x0")
+ }
+
+ initializeSplitTunnelingSettingsUIFingerprint.method.apply {
+ val initSettingsIndex = indexOfFirstInstructionOrThrow {
+ getReference()?.name == "getSplitTunneling"
+ }
+ removeInstruction(initSettingsIndex - 1)
+ }
+ }
+ }
From a429824bb77b49aea14b0b54f2204ae24d5209a1 Mon Sep 17 00:00:00 2001
From: xehpuk
Date: Tue, 23 Dec 2025 02:26:54 +0100
Subject: [PATCH 04/42] fix: Fix compilation error introduced in dc69f243
(#6392)
---
.../youtube/patches/components/LayoutComponentsFilter.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/LayoutComponentsFilter.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/LayoutComponentsFilter.java
index 707e18bc6..4c56466b2 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/LayoutComponentsFilter.java
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/LayoutComponentsFilter.java
@@ -433,7 +433,7 @@ public final class LayoutComponentsFilter extends Filter {
* Injection point.
*/
public static boolean showWatermark() {
- return !Settings.HIDE_VIDEO_CHANNEL_WATERMARK.get();
+ return !Settings.HIDE_CHANNEL_WATERMARK.get();
}
/**
From da02d685875092717b21cf29c53f6401ea7411f6 Mon Sep 17 00:00:00 2001
From: semantic-release-bot
Date: Tue, 23 Dec 2025 01:30:13 +0000
Subject: [PATCH 05/42] chore: Release v5.48.0-dev.1 [skip ci]
# [5.48.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.47.0...v5.48.0-dev.1) (2025-12-23)
### Bug Fixes
* Fix compilation error introduced in dc69f243 ([#6392](https://github.com/ReVanced/revanced-patches/issues/6392)) ([a429824](https://github.com/ReVanced/revanced-patches/commit/a429824bb77b49aea14b0b54f2204ae24d5209a1))
* **YouTube - Hide layout components:** Hide new type of crowdfunding box ([#6380](https://github.com/ReVanced/revanced-patches/issues/6380)) ([dc69f24](https://github.com/ReVanced/revanced-patches/commit/dc69f2433e2650654e2dffdd76b0b0c8a52bf515))
### Features
* **ProtonVPN:** Add `Unlock split tunneling` patch ([#6353](https://github.com/ReVanced/revanced-patches/issues/6353)) ([e0f3346](https://github.com/ReVanced/revanced-patches/commit/e0f33468e6e96b9f10cf35ec67622d6488528c90))
* **SBS On Demand:** Add `Remove ads` patch ([#6378](https://github.com/ReVanced/revanced-patches/issues/6378)) ([315931c](https://github.com/ReVanced/revanced-patches/commit/315931cbf8f61cd4b3a54ace1ff03685d748614c))
---
CHANGELOG.md | 14 ++++++++++++++
gradle.properties | 2 +-
2 files changed, 15 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bf1303913..5449bf6c1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,17 @@
+# [5.48.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.47.0...v5.48.0-dev.1) (2025-12-23)
+
+
+### Bug Fixes
+
+* Fix compilation error introduced in dc69f243 ([#6392](https://github.com/ReVanced/revanced-patches/issues/6392)) ([a429824](https://github.com/ReVanced/revanced-patches/commit/a429824bb77b49aea14b0b54f2204ae24d5209a1))
+* **YouTube - Hide layout components:** Hide new type of crowdfunding box ([#6380](https://github.com/ReVanced/revanced-patches/issues/6380)) ([dc69f24](https://github.com/ReVanced/revanced-patches/commit/dc69f2433e2650654e2dffdd76b0b0c8a52bf515))
+
+
+### Features
+
+* **ProtonVPN:** Add `Unlock split tunneling` patch ([#6353](https://github.com/ReVanced/revanced-patches/issues/6353)) ([e0f3346](https://github.com/ReVanced/revanced-patches/commit/e0f33468e6e96b9f10cf35ec67622d6488528c90))
+* **SBS On Demand:** Add `Remove ads` patch ([#6378](https://github.com/ReVanced/revanced-patches/issues/6378)) ([315931c](https://github.com/ReVanced/revanced-patches/commit/315931cbf8f61cd4b3a54ace1ff03685d748614c))
+
# [5.47.0](https://github.com/ReVanced/revanced-patches/compare/v5.46.0...v5.47.0) (2025-12-18)
diff --git a/gradle.properties b/gradle.properties
index a04789718..946ef5822 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -3,4 +3,4 @@ org.gradle.jvmargs = -Xms512M -Xmx2048M
org.gradle.parallel = true
android.useAndroidX = true
kotlin.code.style = official
-version = 5.47.0
+version = 5.48.0-dev.1
From 8f3f4c95bb8f151fc9a2c272bf7d0e905c2f01fc Mon Sep 17 00:00:00 2001
From: xehpuk
Date: Sat, 27 Dec 2025 18:45:09 +0100
Subject: [PATCH 06/42] feat(Strava): Add `Enable password login` patch (#6396)
Co-authored-by: oSumAtrIX
---
patches/api/patches.api | 4 ++++
.../password/EnablePasswordLoginPatch.kt | 21 +++++++++++++++++++
.../patches/strava/password/Fingerprints.kt | 18 ++++++++++++++++
3 files changed, 43 insertions(+)
create mode 100644 patches/src/main/kotlin/app/revanced/patches/strava/password/EnablePasswordLoginPatch.kt
create mode 100644 patches/src/main/kotlin/app/revanced/patches/strava/password/Fingerprints.kt
diff --git a/patches/api/patches.api b/patches/api/patches.api
index c8438af90..83f2197be 100644
--- a/patches/api/patches.api
+++ b/patches/api/patches.api
@@ -1188,6 +1188,10 @@ public final class app/revanced/patches/stocard/layout/HideStoryBubblesPatchKt {
public static final fun getHideStoryBubblesPatch ()Lapp/revanced/patcher/patch/ResourcePatch;
}
+public final class app/revanced/patches/strava/password/EnablePasswordLoginPatchKt {
+ public static final fun getEnablePasswordLoginPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
+}
+
public final class app/revanced/patches/strava/subscription/UnlockSubscriptionPatchKt {
public static final fun getUnlockSubscriptionPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
diff --git a/patches/src/main/kotlin/app/revanced/patches/strava/password/EnablePasswordLoginPatch.kt b/patches/src/main/kotlin/app/revanced/patches/strava/password/EnablePasswordLoginPatch.kt
new file mode 100644
index 000000000..1d6a62569
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/strava/password/EnablePasswordLoginPatch.kt
@@ -0,0 +1,21 @@
+package app.revanced.patches.strava.password
+
+import app.revanced.patcher.Fingerprint
+import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
+import app.revanced.patcher.patch.bytecodePatch
+
+@Suppress("unused")
+val enablePasswordLoginPatch = bytecodePatch(
+ name = "Enable password login",
+ description = "Re-enables password login after having used an OTP code.",
+) {
+ compatibleWith("com.strava")
+
+ execute {
+ fun Fingerprint.loadTrueInsteadOfField() =
+ method.replaceInstruction(patternMatch!!.startIndex, "const/4 v0, 0x1")
+
+ logInGetUsePasswordFingerprint.loadTrueInsteadOfField()
+ emailChangeGetUsePasswordFingerprint.loadTrueInsteadOfField()
+ }
+}
diff --git a/patches/src/main/kotlin/app/revanced/patches/strava/password/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/strava/password/Fingerprints.kt
new file mode 100644
index 000000000..f5ce86f18
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/strava/password/Fingerprints.kt
@@ -0,0 +1,18 @@
+package app.revanced.patches.strava.password
+
+import app.revanced.patcher.fingerprint
+import com.android.tools.smali.dexlib2.Opcode
+
+internal val logInGetUsePasswordFingerprint = fingerprint {
+ opcodes(Opcode.IGET_BOOLEAN)
+ custom { method, classDef ->
+ method.name == "getUsePassword" && classDef.endsWith("/RequestOtpLogInNetworkResponse;")
+ }
+}
+
+internal val emailChangeGetUsePasswordFingerprint = fingerprint {
+ opcodes(Opcode.IGET_BOOLEAN)
+ custom { method, classDef ->
+ method.name == "getUsePassword" && classDef.endsWith("/RequestEmailChangeWithOtpOrPasswordResponse;")
+ }
+}
From 16bd96e2bb03d51d680f5d4f2f7de3ae28de68bc Mon Sep 17 00:00:00 2001
From: semantic-release-bot
Date: Sat, 27 Dec 2025 17:48:32 +0000
Subject: [PATCH 07/42] chore: Release v5.48.0-dev.2 [skip ci]
# [5.48.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.1...v5.48.0-dev.2) (2025-12-27)
### Features
* **Strava:** Add `Enable password login` patch ([#6396](https://github.com/ReVanced/revanced-patches/issues/6396)) ([8f3f4c9](https://github.com/ReVanced/revanced-patches/commit/8f3f4c95bb8f151fc9a2c272bf7d0e905c2f01fc))
---
CHANGELOG.md | 7 +++++++
gradle.properties | 2 +-
2 files changed, 8 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5449bf6c1..f9abf4644 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,10 @@
+# [5.48.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.1...v5.48.0-dev.2) (2025-12-27)
+
+
+### Features
+
+* **Strava:** Add `Enable password login` patch ([#6396](https://github.com/ReVanced/revanced-patches/issues/6396)) ([8f3f4c9](https://github.com/ReVanced/revanced-patches/commit/8f3f4c95bb8f151fc9a2c272bf7d0e905c2f01fc))
+
# [5.48.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.47.0...v5.48.0-dev.1) (2025-12-23)
diff --git a/gradle.properties b/gradle.properties
index 946ef5822..6f22fbfa4 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -3,4 +3,4 @@ org.gradle.jvmargs = -Xms512M -Xmx2048M
org.gradle.parallel = true
android.useAndroidX = true
kotlin.code.style = official
-version = 5.48.0-dev.1
+version = 5.48.0-dev.2
From 6bb62811493da04812cc3e392e68d874f95cbef9 Mon Sep 17 00:00:00 2001
From: PainfulPaladins <122171814+PainfulPaladins@users.noreply.github.com>
Date: Sat, 27 Dec 2025 19:50:08 +0200
Subject: [PATCH 08/42] feat(Instagram - Hides navigation buttons): Add more
buttons to hide (#6390)
---
.../hide/navigation/HideNavigationButtons.kt | 60 ++++++++++++++++++-
1 file changed, 59 insertions(+), 1 deletion(-)
diff --git a/patches/src/main/kotlin/app/revanced/patches/instagram/hide/navigation/HideNavigationButtons.kt b/patches/src/main/kotlin/app/revanced/patches/instagram/hide/navigation/HideNavigationButtons.kt
index fca74bc18..be7aba78c 100644
--- a/patches/src/main/kotlin/app/revanced/patches/instagram/hide/navigation/HideNavigationButtons.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/instagram/hide/navigation/HideNavigationButtons.kt
@@ -28,6 +28,13 @@ val hideNavigationButtonsPatch = bytecodePatch(
dependsOn(sharedExtensionPatch)
+ val hideHome by booleanOption(
+ key = "hideHome",
+ default = false,
+ title = "Hide Home",
+ description = "Permanently hides the Home button. App starts at next available tab." // On the "homecoming" / current instagram layout.
+ )
+
val hideReels by booleanOption(
key = "hideReels",
default = true,
@@ -35,6 +42,27 @@ val hideNavigationButtonsPatch = bytecodePatch(
description = "Permanently hides the Reels button."
)
+ val hideDirect by booleanOption(
+ key = "hideDirect",
+ default = false,
+ title = "Hide Direct",
+ description = "Permanently hides the Direct button."
+ )
+
+ val hideSearch by booleanOption(
+ key = "hideSearch",
+ default = false,
+ title = "Hide Search",
+ description = "Permanently hides the Search button."
+ )
+
+ val hideProfile by booleanOption(
+ key = "hideProfile",
+ default = false,
+ title = "Hide Profile",
+ description = "Permanently hides the Profile button."
+ )
+
val hideCreate by booleanOption(
key = "hideCreate",
default = true,
@@ -42,8 +70,10 @@ val hideNavigationButtonsPatch = bytecodePatch(
description = "Permanently hides the Create button."
)
+
+
execute {
- if (!hideReels!! && !hideCreate!!) {
+ if (!hideHome!! &&!hideReels!! && !hideDirect!! && !hideSearch!! && !hideProfile && !hideCreate!!) {
return@execute Logger.getLogger(this::class.java.name).warning(
"No hide navigation buttons options are enabled. No changes made."
)
@@ -76,6 +106,13 @@ val hideNavigationButtonsPatch = bytecodePatch(
"""
}
+ if (hideHome!!) {
+ addInstructionsAtControlFlowLabel(
+ returnIndex,
+ instructionsRemoveButtonByName("fragment_feed")
+ )
+ }
+
if (hideReels!!) {
addInstructionsAtControlFlowLabel(
returnIndex,
@@ -83,12 +120,33 @@ val hideNavigationButtonsPatch = bytecodePatch(
)
}
+ if (hideDirect!!) {
+ addInstructionsAtControlFlowLabel(
+ returnIndex,
+ instructionsRemoveButtonByName("fragment_direct_tab")
+ )
+ }
+ if (hideSearch!!) {
+ addInstructionsAtControlFlowLabel(
+ returnIndex,
+ instructionsRemoveButtonByName("fragment_search")
+ )
+ }
+
if (hideCreate!!) {
addInstructionsAtControlFlowLabel(
returnIndex,
instructionsRemoveButtonByName("fragment_share")
)
}
+
+ if (hideProfile!!) {
+ addInstructionsAtControlFlowLabel(
+ returnIndex,
+ instructionsRemoveButtonByName("fragment_profile")
+ )
+ }
+
}
}
}
From 71c6cb569ebf7b93cf73ee391839e5220557ce7c Mon Sep 17 00:00:00 2001
From: xehpuk
Date: Sun, 28 Dec 2025 23:30:32 +0100
Subject: [PATCH 09/42] fix: Fix compilation error introduced in `6bb6281`
(#6409)
---
.../instagram/hide/navigation/HideNavigationButtons.kt | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/patches/src/main/kotlin/app/revanced/patches/instagram/hide/navigation/HideNavigationButtons.kt b/patches/src/main/kotlin/app/revanced/patches/instagram/hide/navigation/HideNavigationButtons.kt
index be7aba78c..b946abf6b 100644
--- a/patches/src/main/kotlin/app/revanced/patches/instagram/hide/navigation/HideNavigationButtons.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/instagram/hide/navigation/HideNavigationButtons.kt
@@ -70,10 +70,8 @@ val hideNavigationButtonsPatch = bytecodePatch(
description = "Permanently hides the Create button."
)
-
-
execute {
- if (!hideHome!! &&!hideReels!! && !hideDirect!! && !hideSearch!! && !hideProfile && !hideCreate!!) {
+ if (!hideHome!! &&!hideReels!! && !hideDirect!! && !hideSearch!! && !hideProfile!! && !hideCreate!!) {
return@execute Logger.getLogger(this::class.java.name).warning(
"No hide navigation buttons options are enabled. No changes made."
)
From cebcfab86a0214462388117e528e8f42f0e9fb74 Mon Sep 17 00:00:00 2001
From: semantic-release-bot
Date: Sun, 28 Dec 2025 22:33:37 +0000
Subject: [PATCH 10/42] chore: Release v5.48.0-dev.3 [skip ci]
# [5.48.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.2...v5.48.0-dev.3) (2025-12-28)
### Bug Fixes
* Fix compilation error introduced in `6bb6281` ([#6409](https://github.com/ReVanced/revanced-patches/issues/6409)) ([71c6cb5](https://github.com/ReVanced/revanced-patches/commit/71c6cb569ebf7b93cf73ee391839e5220557ce7c))
### Features
* **Instagram - Hides navigation buttons:** Add more buttons to hide ([#6390](https://github.com/ReVanced/revanced-patches/issues/6390)) ([6bb6281](https://github.com/ReVanced/revanced-patches/commit/6bb62811493da04812cc3e392e68d874f95cbef9))
---
CHANGELOG.md | 12 ++++++++++++
gradle.properties | 2 +-
2 files changed, 13 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f9abf4644..4558f1ae7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,15 @@
+# [5.48.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.2...v5.48.0-dev.3) (2025-12-28)
+
+
+### Bug Fixes
+
+* Fix compilation error introduced in `6bb6281` ([#6409](https://github.com/ReVanced/revanced-patches/issues/6409)) ([71c6cb5](https://github.com/ReVanced/revanced-patches/commit/71c6cb569ebf7b93cf73ee391839e5220557ce7c))
+
+
+### Features
+
+* **Instagram - Hides navigation buttons:** Add more buttons to hide ([#6390](https://github.com/ReVanced/revanced-patches/issues/6390)) ([6bb6281](https://github.com/ReVanced/revanced-patches/commit/6bb62811493da04812cc3e392e68d874f95cbef9))
+
# [5.48.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.1...v5.48.0-dev.2) (2025-12-27)
diff --git a/gradle.properties b/gradle.properties
index 6f22fbfa4..a51abcd76 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -3,4 +3,4 @@ org.gradle.jvmargs = -Xms512M -Xmx2048M
org.gradle.parallel = true
android.useAndroidX = true
kotlin.code.style = official
-version = 5.48.0-dev.2
+version = 5.48.0-dev.3
From c47beae21376dd17ab8bc09afe73e9094481bde9 Mon Sep 17 00:00:00 2001
From: xehpuk
Date: Mon, 29 Dec 2025 22:20:26 +0100
Subject: [PATCH 11/42] feat(Strava): Add `Block Snowplow tracking` patch
(#6413)
Co-authored-by: oSumAtrIX
---
patches/api/patches.api | 4 ++++
.../privacy/BlockSnowplowTrackingPatch.kt | 17 +++++++++++++++++
.../patches/strava/privacy/Fingerprints.kt | 9 +++++++++
3 files changed, 30 insertions(+)
create mode 100644 patches/src/main/kotlin/app/revanced/patches/strava/privacy/BlockSnowplowTrackingPatch.kt
create mode 100644 patches/src/main/kotlin/app/revanced/patches/strava/privacy/Fingerprints.kt
diff --git a/patches/api/patches.api b/patches/api/patches.api
index 83f2197be..933e267e5 100644
--- a/patches/api/patches.api
+++ b/patches/api/patches.api
@@ -1192,6 +1192,10 @@ public final class app/revanced/patches/strava/password/EnablePasswordLoginPatch
public static final fun getEnablePasswordLoginPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
+public final class app/revanced/patches/strava/snowplow/BlockSnowplowTrackingPatchKt {
+ public static final fun getBlockSnowplowTrackingPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
+}
+
public final class app/revanced/patches/strava/subscription/UnlockSubscriptionPatchKt {
public static final fun getUnlockSubscriptionPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
diff --git a/patches/src/main/kotlin/app/revanced/patches/strava/privacy/BlockSnowplowTrackingPatch.kt b/patches/src/main/kotlin/app/revanced/patches/strava/privacy/BlockSnowplowTrackingPatch.kt
new file mode 100644
index 000000000..313a98d1d
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/strava/privacy/BlockSnowplowTrackingPatch.kt
@@ -0,0 +1,17 @@
+package app.revanced.patches.strava.privacy
+
+import app.revanced.patcher.patch.bytecodePatch
+import app.revanced.util.returnEarly
+
+@Suppress("unused")
+val blockSnowplowTrackingPatch = bytecodePatch(
+ name = "Block Snowplow tracking",
+ description = "Blocks Snowplow analytics. See https://snowplow.io for more information.",
+) {
+ compatibleWith("com.strava")
+
+ execute {
+ // Keep events list empty, otherwise sent to https://c.strava.com/com.snowplowanalytics.snowplow/tp2.
+ insertEventFingerprint.method.returnEarly()
+ }
+}
diff --git a/patches/src/main/kotlin/app/revanced/patches/strava/privacy/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/strava/privacy/Fingerprints.kt
new file mode 100644
index 000000000..196602ba0
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/strava/privacy/Fingerprints.kt
@@ -0,0 +1,9 @@
+package app.revanced.patches.strava.privacy
+
+import app.revanced.patcher.fingerprint
+
+// https://github.com/snowplow/snowplow-android-tracker/blob/2.2.0/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/internal/emitter/storage/SQLiteEventStore.java#L130
+// Not the exact same code (e.g. returns void instead of long), even though the version number matches.
+internal val insertEventFingerprint = fingerprint {
+ strings("Added event to database: %s")
+}
From 195c239000e03bf87ea81a397aa3f4d927529b8f Mon Sep 17 00:00:00 2001
From: semantic-release-bot
Date: Mon, 29 Dec 2025 21:25:35 +0000
Subject: [PATCH 12/42] chore: Release v5.48.0-dev.4 [skip ci]
# [5.48.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.3...v5.48.0-dev.4) (2025-12-29)
### Features
* **Strava:** Add `Block Snowplow tracking` patch ([#6413](https://github.com/ReVanced/revanced-patches/issues/6413)) ([c47beae](https://github.com/ReVanced/revanced-patches/commit/c47beae21376dd17ab8bc09afe73e9094481bde9))
---
CHANGELOG.md | 7 +++++++
gradle.properties | 2 +-
2 files changed, 8 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4558f1ae7..e4a594855 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,10 @@
+# [5.48.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.3...v5.48.0-dev.4) (2025-12-29)
+
+
+### Features
+
+* **Strava:** Add `Block Snowplow tracking` patch ([#6413](https://github.com/ReVanced/revanced-patches/issues/6413)) ([c47beae](https://github.com/ReVanced/revanced-patches/commit/c47beae21376dd17ab8bc09afe73e9094481bde9))
+
# [5.48.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.2...v5.48.0-dev.3) (2025-12-28)
diff --git a/gradle.properties b/gradle.properties
index a51abcd76..00a1ebc1d 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -3,4 +3,4 @@ org.gradle.jvmargs = -Xms512M -Xmx2048M
org.gradle.parallel = true
android.useAndroidX = true
kotlin.code.style = official
-version = 5.48.0-dev.3
+version = 5.48.0-dev.4
From 44e7dbcf4d7eaf94dd0164baba847d3e19250154 Mon Sep 17 00:00:00 2001
From: ILoveOpenSourceApplications
<117499019+ILoveOpenSourceApplications@users.noreply.github.com>
Date: Wed, 31 Dec 2025 00:02:17 +0530
Subject: [PATCH 13/42] fix(Disney+ - Skip ads): Remove unsupported package
names (#6422)
---
.../kotlin/app/revanced/patches/disneyplus/SkipAdsPatch.kt | 6 +-----
1 file changed, 1 insertion(+), 5 deletions(-)
diff --git a/patches/src/main/kotlin/app/revanced/patches/disneyplus/SkipAdsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/disneyplus/SkipAdsPatch.kt
index e93cafc0d..5b0f551cd 100644
--- a/patches/src/main/kotlin/app/revanced/patches/disneyplus/SkipAdsPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/disneyplus/SkipAdsPatch.kt
@@ -8,11 +8,7 @@ val skipAdsPatch = bytecodePatch(
name = "Skip ads",
description = "Automatically skips ads.",
) {
- compatibleWith(
- "com.disney.disneyplus",
- "in.startv.hotstar",
- "in.startv.hotstaronly",
- )
+ compatibleWith("com.disney.disneyplus")
execute {
arrayOf(insertionGetPointsFingerprint, insertionGetRangesFingerprint).forEach {
From da836b667c455f6e77a73b41460bd281dac0943c Mon Sep 17 00:00:00 2001
From: semantic-release-bot
Date: Tue, 30 Dec 2025 18:37:28 +0000
Subject: [PATCH 14/42] chore: Release v5.48.0-dev.5 [skip ci]
# [5.48.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.4...v5.48.0-dev.5) (2025-12-30)
### Bug Fixes
* **Disney+ - Skip ads:** Remove unsupported package names ([#6422](https://github.com/ReVanced/revanced-patches/issues/6422)) ([44e7dbc](https://github.com/ReVanced/revanced-patches/commit/44e7dbcf4d7eaf94dd0164baba847d3e19250154))
---
CHANGELOG.md | 7 +++++++
gradle.properties | 2 +-
2 files changed, 8 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e4a594855..608009cb8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,10 @@
+# [5.48.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.4...v5.48.0-dev.5) (2025-12-30)
+
+
+### Bug Fixes
+
+* **Disney+ - Skip ads:** Remove unsupported package names ([#6422](https://github.com/ReVanced/revanced-patches/issues/6422)) ([44e7dbc](https://github.com/ReVanced/revanced-patches/commit/44e7dbcf4d7eaf94dd0164baba847d3e19250154))
+
# [5.48.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.3...v5.48.0-dev.4) (2025-12-29)
diff --git a/gradle.properties b/gradle.properties
index 00a1ebc1d..97544a540 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -3,4 +3,4 @@ org.gradle.jvmargs = -Xms512M -Xmx2048M
org.gradle.parallel = true
android.useAndroidX = true
kotlin.code.style = official
-version = 5.48.0-dev.4
+version = 5.48.0-dev.5
From 789f0a562861825065633d172445ebf35a1ba8d8 Mon Sep 17 00:00:00 2001
From: xehpuk
Date: Sun, 4 Jan 2026 03:03:44 +0100
Subject: [PATCH 15/42] fix: Fix build error introduced in `4046bee` (#6417)
---
patches/api/patches.api | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/patches/api/patches.api b/patches/api/patches.api
index 933e267e5..b2d0eaa09 100644
--- a/patches/api/patches.api
+++ b/patches/api/patches.api
@@ -1192,7 +1192,7 @@ public final class app/revanced/patches/strava/password/EnablePasswordLoginPatch
public static final fun getEnablePasswordLoginPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
-public final class app/revanced/patches/strava/snowplow/BlockSnowplowTrackingPatchKt {
+public final class app/revanced/patches/strava/privacy/BlockSnowplowTrackingPatchKt {
public static final fun getBlockSnowplowTrackingPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
From 43ab29d03d82b55f06e93290b4f4e59bef58f599 Mon Sep 17 00:00:00 2001
From: semantic-release-bot
Date: Sun, 4 Jan 2026 02:07:07 +0000
Subject: [PATCH 16/42] chore: Release v5.48.0-dev.6 [skip ci]
# [5.48.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.5...v5.48.0-dev.6) (2026-01-04)
### Bug Fixes
* Fix build error introduced in `4046bee` ([#6417](https://github.com/ReVanced/revanced-patches/issues/6417)) ([789f0a5](https://github.com/ReVanced/revanced-patches/commit/789f0a562861825065633d172445ebf35a1ba8d8))
---
CHANGELOG.md | 7 +++++++
gradle.properties | 2 +-
2 files changed, 8 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 608009cb8..cd5f243ea 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,10 @@
+# [5.48.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.5...v5.48.0-dev.6) (2026-01-04)
+
+
+### Bug Fixes
+
+* Fix build error introduced in `4046bee` ([#6417](https://github.com/ReVanced/revanced-patches/issues/6417)) ([789f0a5](https://github.com/ReVanced/revanced-patches/commit/789f0a562861825065633d172445ebf35a1ba8d8))
+
# [5.48.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.4...v5.48.0-dev.5) (2025-12-30)
diff --git a/gradle.properties b/gradle.properties
index 97544a540..38f8c15ff 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -3,4 +3,4 @@ org.gradle.jvmargs = -Xms512M -Xmx2048M
org.gradle.parallel = true
android.useAndroidX = true
kotlin.code.style = official
-version = 5.48.0-dev.5
+version = 5.48.0-dev.6
From b42ae27ce66ebad9e9cfc5b70fc121df5bad7567 Mon Sep 17 00:00:00 2001
From: xehpuk
Date: Sun, 4 Jan 2026 03:36:08 +0100
Subject: [PATCH 17/42] feat(Strava): Add `Overwrite media upload parameters`
patch (#6410)
---
patches/api/patches.api | 6 +-
.../strava/mediaupload/Fingerprints.kt | 21 +
.../OverwriteMediaUploadParametersPatch.kt | 48 ++
.../password/EnablePasswordLoginPatch.kt | 9 +-
.../patches/strava/password/Fingerprints.kt | 3 -
.../strava/subscription/Fingerprints.kt | 4 +-
.../subscription/UnlockSubscriptionPatch.kt | 7 +-
.../kotlin/app/revanced/util/BytecodeUtils.kt | 519 +++++++++++-------
.../main/kotlin/app/revanced/util/Utils.kt | 19 +-
9 files changed, 407 insertions(+), 229 deletions(-)
create mode 100644 patches/src/main/kotlin/app/revanced/patches/strava/mediaupload/Fingerprints.kt
create mode 100644 patches/src/main/kotlin/app/revanced/patches/strava/mediaupload/OverwriteMediaUploadParametersPatch.kt
diff --git a/patches/api/patches.api b/patches/api/patches.api
index b2d0eaa09..63a56070a 100644
--- a/patches/api/patches.api
+++ b/patches/api/patches.api
@@ -1188,6 +1188,10 @@ public final class app/revanced/patches/stocard/layout/HideStoryBubblesPatchKt {
public static final fun getHideStoryBubblesPatch ()Lapp/revanced/patcher/patch/ResourcePatch;
}
+public final class app/revanced/patches/strava/mediaupload/OverwriteMediaUploadParametersPatchKt {
+ public static final fun getOverwriteMediaUploadParametersPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
+}
+
public final class app/revanced/patches/strava/password/EnablePasswordLoginPatchKt {
public static final fun getEnablePasswordLoginPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
@@ -1971,6 +1975,7 @@ public final class app/revanced/util/BytecodeUtilsKt {
public static final fun indexOfFirstResourceIdOrThrow (Lcom/android/tools/smali/dexlib2/iface/Method;Ljava/lang/String;)I
public static final fun injectHideViewCall (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;IILjava/lang/String;Ljava/lang/String;)V
public static final fun literal (Lapp/revanced/patcher/FingerprintBuilder;Lkotlin/jvm/functions/Function0;)V
+ public static final fun returnEarly (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;)V
public static final fun returnEarly (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;B)V
public static final fun returnEarly (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;C)V
public static final fun returnEarly (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;D)V
@@ -1980,7 +1985,6 @@ public final class app/revanced/util/BytecodeUtilsKt {
public static final fun returnEarly (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;Ljava/lang/String;)V
public static final fun returnEarly (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;S)V
public static final fun returnEarly (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;Z)V
- public static synthetic fun returnEarly$default (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;ZILjava/lang/Object;)V
public static final fun returnLate (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;B)V
public static final fun returnLate (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;C)V
public static final fun returnLate (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;D)V
diff --git a/patches/src/main/kotlin/app/revanced/patches/strava/mediaupload/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/strava/mediaupload/Fingerprints.kt
new file mode 100644
index 000000000..7901d6cc5
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/strava/mediaupload/Fingerprints.kt
@@ -0,0 +1,21 @@
+package app.revanced.patches.strava.mediaupload
+
+import app.revanced.patcher.fingerprint
+
+internal val getCompressionQualityFingerprint = fingerprint {
+ custom { method, _ ->
+ method.name == "getCompressionQuality"
+ }
+}
+
+internal val getMaxDurationFingerprint = fingerprint {
+ custom { method, _ ->
+ method.name == "getMaxDuration"
+ }
+}
+
+internal val getMaxSizeFingerprint = fingerprint {
+ custom { method, _ ->
+ method.name == "getMaxSize"
+ }
+}
diff --git a/patches/src/main/kotlin/app/revanced/patches/strava/mediaupload/OverwriteMediaUploadParametersPatch.kt b/patches/src/main/kotlin/app/revanced/patches/strava/mediaupload/OverwriteMediaUploadParametersPatch.kt
new file mode 100644
index 000000000..182306c6d
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/strava/mediaupload/OverwriteMediaUploadParametersPatch.kt
@@ -0,0 +1,48 @@
+package app.revanced.patches.strava.mediaupload
+
+import app.revanced.patcher.patch.bytecodePatch
+import app.revanced.patcher.patch.intOption
+import app.revanced.patcher.patch.longOption
+import app.revanced.util.returnEarly
+
+@Suppress("unused")
+val overwriteMediaUploadParametersPatch = bytecodePatch(
+ name = "Overwrite media upload parameters",
+ description = "Overwrites the compression, resize and trim media (images and videos) parameters returned by Strava's server before upload.",
+) {
+ compatibleWith("com.strava")
+
+ val compressionQuality by intOption(
+ key = "compressionQuality",
+ title = "Compression quality (percent)",
+ description = "This is used as the JPEG quality setting (≤ 100).",
+ ) { it == null || it in 1..100 }
+
+ val maxDuration by longOption(
+ key = "maxDuration",
+ title = "Max duration (seconds)",
+ description = "The maximum length (≤ ${60 * 60}) of a video before it gets trimmed.",
+ ) { it == null || it in 1..60 * 60 }
+
+ val maxSize by intOption(
+ key = "maxSize",
+ title = "Max size (pixels)",
+ description = "The image gets resized so that the smaller dimension (width/height) does not exceed this value (≤ 10000).",
+ ) { it == null || it in 1..10000 }
+
+ execute {
+ val mediaUploadParametersClass = classes.single { it.endsWith("/MediaUploadParameters;") }
+
+ compressionQuality?.let { compressionQuality ->
+ getCompressionQualityFingerprint.match(mediaUploadParametersClass).method.returnEarly(compressionQuality / 100f)
+ }
+
+ maxDuration?.let { maxDuration ->
+ getMaxDurationFingerprint.match(mediaUploadParametersClass).method.returnEarly(maxDuration)
+ }
+
+ maxSize?.let {
+ getMaxSizeFingerprint.match(mediaUploadParametersClass).method.returnEarly(it)
+ }
+ }
+}
diff --git a/patches/src/main/kotlin/app/revanced/patches/strava/password/EnablePasswordLoginPatch.kt b/patches/src/main/kotlin/app/revanced/patches/strava/password/EnablePasswordLoginPatch.kt
index 1d6a62569..6b7e74235 100644
--- a/patches/src/main/kotlin/app/revanced/patches/strava/password/EnablePasswordLoginPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/strava/password/EnablePasswordLoginPatch.kt
@@ -1,8 +1,8 @@
package app.revanced.patches.strava.password
import app.revanced.patcher.Fingerprint
-import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
import app.revanced.patcher.patch.bytecodePatch
+import app.revanced.util.returnEarly
@Suppress("unused")
val enablePasswordLoginPatch = bytecodePatch(
@@ -12,10 +12,9 @@ val enablePasswordLoginPatch = bytecodePatch(
compatibleWith("com.strava")
execute {
- fun Fingerprint.loadTrueInsteadOfField() =
- method.replaceInstruction(patternMatch!!.startIndex, "const/4 v0, 0x1")
+ fun Fingerprint.returnTrue() = method.returnEarly(true)
- logInGetUsePasswordFingerprint.loadTrueInsteadOfField()
- emailChangeGetUsePasswordFingerprint.loadTrueInsteadOfField()
+ logInGetUsePasswordFingerprint.returnTrue()
+ emailChangeGetUsePasswordFingerprint.returnTrue()
}
}
diff --git a/patches/src/main/kotlin/app/revanced/patches/strava/password/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/strava/password/Fingerprints.kt
index f5ce86f18..94c88490a 100644
--- a/patches/src/main/kotlin/app/revanced/patches/strava/password/Fingerprints.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/strava/password/Fingerprints.kt
@@ -1,17 +1,14 @@
package app.revanced.patches.strava.password
import app.revanced.patcher.fingerprint
-import com.android.tools.smali.dexlib2.Opcode
internal val logInGetUsePasswordFingerprint = fingerprint {
- opcodes(Opcode.IGET_BOOLEAN)
custom { method, classDef ->
method.name == "getUsePassword" && classDef.endsWith("/RequestOtpLogInNetworkResponse;")
}
}
internal val emailChangeGetUsePasswordFingerprint = fingerprint {
- opcodes(Opcode.IGET_BOOLEAN)
custom { method, classDef ->
method.name == "getUsePassword" && classDef.endsWith("/RequestEmailChangeWithOtpOrPasswordResponse;")
}
diff --git a/patches/src/main/kotlin/app/revanced/patches/strava/subscription/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/strava/subscription/Fingerprints.kt
index 0458f45d3..45583ce4e 100644
--- a/patches/src/main/kotlin/app/revanced/patches/strava/subscription/Fingerprints.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/strava/subscription/Fingerprints.kt
@@ -1,11 +1,9 @@
package app.revanced.patches.strava.subscription
import app.revanced.patcher.fingerprint
-import com.android.tools.smali.dexlib2.Opcode
internal val getSubscribedFingerprint = fingerprint {
- opcodes(Opcode.IGET_BOOLEAN)
custom { method, classDef ->
- classDef.endsWith("/SubscriptionDetailResponse;") && method.name == "getSubscribed"
+ method.name == "getSubscribed" && classDef.endsWith("/SubscriptionDetailResponse;")
}
}
diff --git a/patches/src/main/kotlin/app/revanced/patches/strava/subscription/UnlockSubscriptionPatch.kt b/patches/src/main/kotlin/app/revanced/patches/strava/subscription/UnlockSubscriptionPatch.kt
index e59660472..e9fbc49a4 100644
--- a/patches/src/main/kotlin/app/revanced/patches/strava/subscription/UnlockSubscriptionPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/strava/subscription/UnlockSubscriptionPatch.kt
@@ -1,7 +1,7 @@
package app.revanced.patches.strava.subscription
-import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
import app.revanced.patcher.patch.bytecodePatch
+import app.revanced.util.returnEarly
@Suppress("unused")
val unlockSubscriptionPatch = bytecodePatch(
@@ -11,9 +11,6 @@ val unlockSubscriptionPatch = bytecodePatch(
compatibleWith("com.strava")
execute {
- getSubscribedFingerprint.method.replaceInstruction(
- getSubscribedFingerprint.patternMatch!!.startIndex,
- "const/4 v0, 0x1",
- )
+ getSubscribedFingerprint.method.returnEarly(true)
}
}
diff --git a/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt b/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt
index 993fa820b..f5a39a996 100644
--- a/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt
+++ b/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt
@@ -23,22 +23,19 @@ import app.revanced.util.InstructionUtils.Companion.writeOpcodes
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.Opcode.*
+import com.android.tools.smali.dexlib2.analysis.reflection.util.ReflectionUtils
+import com.android.tools.smali.dexlib2.formatter.DexFormatter
import com.android.tools.smali.dexlib2.iface.Method
-import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction
-import com.android.tools.smali.dexlib2.iface.instruction.Instruction
-import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
-import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
-import com.android.tools.smali.dexlib2.iface.instruction.RegisterRangeInstruction
-import com.android.tools.smali.dexlib2.iface.instruction.ThreeRegisterInstruction
-import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction
-import com.android.tools.smali.dexlib2.iface.instruction.WideLiteralInstruction
+import com.android.tools.smali.dexlib2.iface.instruction.*
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
import com.android.tools.smali.dexlib2.iface.reference.Reference
import com.android.tools.smali.dexlib2.iface.reference.StringReference
+import com.android.tools.smali.dexlib2.iface.value.*
import com.android.tools.smali.dexlib2.immutable.ImmutableField
+import com.android.tools.smali.dexlib2.immutable.value.*
import com.android.tools.smali.dexlib2.util.MethodUtil
-import java.util.EnumSet
+import java.util.*
/**
* Starting from and including the instruction at index [startIndex],
@@ -180,7 +177,7 @@ internal val Instruction.isReturnInstruction: Boolean
*
* @param fieldName The name of the field to find. Partial matches are allowed.
*/
-private fun Method.findInstructionIndexFromToString(fieldName: String) : Int {
+private fun Method.findInstructionIndexFromToString(fieldName: String): Int {
val stringIndex = indexOfFirstInstruction {
val reference = getReference()
reference?.string?.contains(fieldName) == true
@@ -233,7 +230,7 @@ private fun Method.findInstructionIndexFromToString(fieldName: String) : Int {
* @param fieldName The name of the field to find. Partial matches are allowed.
*/
context(BytecodePatchContext)
-internal fun Method.findMethodFromToString(fieldName: String) : MutableMethod {
+internal fun Method.findMethodFromToString(fieldName: String): MutableMethod {
val methodUsageIndex = findInstructionIndexFromToString(fieldName)
return navigate(this).to(methodUsageIndex).stop()
}
@@ -243,7 +240,7 @@ internal fun Method.findMethodFromToString(fieldName: String) : MutableMethod {
*
* @param fieldName The name of the field to find. Partial matches are allowed.
*/
-internal fun Method.findFieldFromToString(fieldName: String) : FieldReference {
+internal fun Method.findFieldFromToString(fieldName: String): FieldReference {
val methodUsageIndex = findInstructionIndexFromToString(fieldName)
return getInstruction(methodUsageIndex).getReference()!!
}
@@ -838,23 +835,59 @@ fun BytecodePatchContext.forEachLiteralValueInstruction(
}
-private const val RETURN_TYPE_MISMATCH = "Mismatch between override type and Method return type"
+private fun MutableMethod.checkReturnType(expectedTypes: Iterable>) {
+ val returnTypeJava = ReflectionUtils.dexToJavaName(returnType)
+ check(expectedTypes.any { returnTypeJava == it.name }) {
+ "Actual return type $returnTypeJava is not contained in expected types: $expectedTypes"
+ }
+}
/**
- * Overrides the first instruction of a method with a constant `Boolean` return value.
+ * Overrides the first instruction of a method with returning the default value for the type (or `void`).
* None of the method code will ever execute.
*
- * For methods that return an object or any array type, calling this method with `false`
- * will force the method to return a `null` value.
- *
* @see returnLate
*/
-fun MutableMethod.returnEarly(value: Boolean = false) {
- val returnType = returnType.first()
- check(returnType == 'Z' || (!value && (returnType == 'V' || returnType == 'L' || returnType != '['))) {
- RETURN_TYPE_MISMATCH
+fun MutableMethod.returnEarly() {
+ val value = when (returnType) {
+ "V" -> null
+ "Z" -> ImmutableBooleanEncodedValue.FALSE_VALUE
+ "B" -> ImmutableByteEncodedValue(0)
+ "S" -> ImmutableShortEncodedValue(0)
+ "C" -> ImmutableCharEncodedValue(Char.MIN_VALUE)
+ "I" -> ImmutableIntEncodedValue(0)
+ "F" -> ImmutableFloatEncodedValue(0f)
+ "J" -> ImmutableLongEncodedValue(0)
+ "D" -> ImmutableDoubleEncodedValue(0.0)
+ else -> ImmutableNullEncodedValue.INSTANCE
}
- overrideReturnValue(value.toHexString(), false)
+ overrideReturnValue(value, false)
+}
+
+private fun MutableMethod.returnString(value: String, late: Boolean) {
+ checkReturnType(String::class.java.allAssignableTypes())
+ overrideReturnValue(ImmutableStringEncodedValue(value), late)
+}
+
+/**
+ * Overrides the first instruction of a method with a constant `String` return value.
+ * None of the method code will ever execute.
+ *
+ * @see returnLate
+ */
+fun MutableMethod.returnEarly(value: String) = returnString(value, false)
+
+/**
+ * Overrides all return statements with a constant `String` value.
+ * All method code is executed the same as unpatched.
+ *
+ * @see returnEarly
+ */
+fun MutableMethod.returnLate(value: String) = returnString(value, true)
+
+private fun MutableMethod.returnByte(value: Byte, late: Boolean) {
+ checkReturnType(Byte::class.javaObjectType.allAssignableTypes() + Byte::class.javaPrimitiveType!!)
+ overrideReturnValue(ImmutableByteEncodedValue(value), late)
}
/**
@@ -863,9 +896,40 @@ fun MutableMethod.returnEarly(value: Boolean = false) {
*
* @see returnLate
*/
-fun MutableMethod.returnEarly(value: Byte) {
- check(returnType.first() == 'B') { RETURN_TYPE_MISMATCH }
- overrideReturnValue(value.toString(), false)
+fun MutableMethod.returnEarly(value: Byte) = returnByte(value, false)
+
+/**
+ * Overrides all return statements with a constant `Byte` value.
+ * All method code is executed the same as unpatched.
+ *
+ * @see returnEarly
+ */
+fun MutableMethod.returnLate(value: Byte) = returnByte(value, true)
+
+private fun MutableMethod.returnBoolean(value: Boolean, late: Boolean) {
+ checkReturnType(Boolean::class.javaObjectType.allAssignableTypes() + Boolean::class.javaPrimitiveType!!)
+ overrideReturnValue(ImmutableBooleanEncodedValue.forBoolean(value), late)
+}
+
+/**
+ * Overrides the first instruction of a method with a constant `Boolean` return value.
+ * None of the method code will ever execute.
+ *
+ * @see returnLate
+ */
+fun MutableMethod.returnEarly(value: Boolean) = returnBoolean(value, false)
+
+/**
+ * Overrides all return statements with a constant `Boolean` value.
+ * All method code is executed the same as unpatched.
+ *
+ * @see returnEarly
+ */
+fun MutableMethod.returnLate(value: Boolean) = returnBoolean(value, true)
+
+private fun MutableMethod.returnShort(value: Short, late: Boolean) {
+ checkReturnType(Short::class.javaObjectType.allAssignableTypes() + Short::class.javaPrimitiveType!!)
+ overrideReturnValue(ImmutableShortEncodedValue(value), late)
}
/**
@@ -874,9 +938,19 @@ fun MutableMethod.returnEarly(value: Byte) {
*
* @see returnLate
*/
-fun MutableMethod.returnEarly(value: Short) {
- check(returnType.first() == 'S') { RETURN_TYPE_MISMATCH }
- overrideReturnValue(value.toString(), false)
+fun MutableMethod.returnEarly(value: Short) = returnShort(value, false)
+
+/**
+ * Overrides all return statements with a constant `Short` value.
+ * All method code is executed the same as unpatched.
+ *
+ * @see returnEarly
+ */
+fun MutableMethod.returnLate(value: Short) = returnShort(value, true)
+
+private fun MutableMethod.returnChar(value: Char, late: Boolean) {
+ checkReturnType(Char::class.javaObjectType.allAssignableTypes() + Char::class.javaPrimitiveType!!)
+ overrideReturnValue(ImmutableCharEncodedValue(value), late)
}
/**
@@ -885,9 +959,19 @@ fun MutableMethod.returnEarly(value: Short) {
*
* @see returnLate
*/
-fun MutableMethod.returnEarly(value: Char) {
- check(returnType.first() == 'C') { RETURN_TYPE_MISMATCH }
- overrideReturnValue(value.code.toString(), false)
+fun MutableMethod.returnEarly(value: Char) = returnChar(value, false)
+
+/**
+ * Overrides all return statements with a constant `Char` value.
+ * All method code is executed the same as unpatched.
+ *
+ * @see returnEarly
+ */
+fun MutableMethod.returnLate(value: Char) = returnChar(value, true)
+
+private fun MutableMethod.returnInt(value: Int, late: Boolean) {
+ checkReturnType(Int::class.javaObjectType.allAssignableTypes() + Int::class.javaPrimitiveType!!)
+ overrideReturnValue(ImmutableIntEncodedValue(value), late)
}
/**
@@ -896,20 +980,19 @@ fun MutableMethod.returnEarly(value: Char) {
*
* @see returnLate
*/
-fun MutableMethod.returnEarly(value: Int) {
- check(returnType.first() == 'I') { RETURN_TYPE_MISMATCH }
- overrideReturnValue(value.toString(), false)
-}
+fun MutableMethod.returnEarly(value: Int) = returnInt(value, false)
/**
- * Overrides the first instruction of a method with a constant `Long` return value.
- * None of the method code will ever execute.
+ * Overrides all return statements with a constant `Int` value.
+ * All method code is executed the same as unpatched.
*
- * @see returnLate
+ * @see returnEarly
*/
-fun MutableMethod.returnEarly(value: Long) {
- check(returnType.first() == 'J') { RETURN_TYPE_MISMATCH }
- overrideReturnValue(value.toString(), false)
+fun MutableMethod.returnLate(value: Int) = returnInt(value, true)
+
+private fun MutableMethod.returnFloat(value: Float, late: Boolean) {
+ checkReturnType(Float::class.javaObjectType.allAssignableTypes() + Float::class.javaPrimitiveType!!)
+ overrideReturnValue(ImmutableFloatEncodedValue(value), late)
}
/**
@@ -918,9 +1001,40 @@ fun MutableMethod.returnEarly(value: Long) {
*
* @see returnLate
*/
-fun MutableMethod.returnEarly(value: Float) {
- check(returnType.first() == 'F') { RETURN_TYPE_MISMATCH }
- overrideReturnValue(value.toString(), false)
+fun MutableMethod.returnEarly(value: Float) = returnFloat(value, false)
+
+/**
+ * Overrides all return statements with a constant `Float` value.
+ * All method code is executed the same as unpatched.
+ *
+ * @see returnEarly
+ */
+fun MutableMethod.returnLate(value: Float) = returnFloat(value, true)
+
+private fun MutableMethod.returnLong(value: Long, late: Boolean) {
+ checkReturnType(Long::class.javaObjectType.allAssignableTypes() + Long::class.javaPrimitiveType!!)
+ overrideReturnValue(ImmutableLongEncodedValue(value), late)
+}
+
+/**
+ * Overrides the first instruction of a method with a constant `Long` return value.
+ * None of the method code will ever execute.
+ *
+ * @see returnLate
+ */
+fun MutableMethod.returnEarly(value: Long) = returnLong(value, false)
+
+/**
+ * Overrides all return statements with a constant `Long` value.
+ * All method code is executed the same as unpatched.
+ *
+ * @see returnEarly
+ */
+fun MutableMethod.returnLate(value: Long) = returnLong(value, true)
+
+private fun MutableMethod.returnDouble(value: Double, late: Boolean) {
+ checkReturnType(Double::class.javaObjectType.allAssignableTypes() + Double::class.javaPrimitiveType!!)
+ overrideReturnValue(ImmutableDoubleEncodedValue(value), late)
}
/**
@@ -929,113 +1043,7 @@ fun MutableMethod.returnEarly(value: Float) {
*
* @see returnLate
*/
-fun MutableMethod.returnEarly(value: Double) {
- check(returnType.first() == 'J') { RETURN_TYPE_MISMATCH }
- overrideReturnValue(value.toString(), false)
-}
-
-/**
- * Overrides the first instruction of a method with a constant String return value.
- * None of the method code will ever execute.
- *
- * Target method must have return type
- * Ljava/lang/String; or Ljava/lang/CharSequence;
- *
- * @see returnLate
- */
-fun MutableMethod.returnEarly(value: String) {
- check(returnType == "Ljava/lang/String;" || returnType == "Ljava/lang/CharSequence;") {
- RETURN_TYPE_MISMATCH
- }
- overrideReturnValue(value, false)
-}
-
-/**
- * Overrides all return statements with a constant `Boolean` value.
- * All method code is executed the same as unpatched.
- *
- * For methods that return an object or any array type, calling this method with `false`
- * will force the method to return a `null` value.
- *
- * @see returnEarly
- */
-fun MutableMethod.returnLate(value: Boolean) {
- val returnType = returnType.first()
- if (returnType == 'V') {
- error("Cannot return late for Method of void type")
- }
- check(returnType == 'Z' || (!value && (returnType == 'L' || returnType == '['))) {
- RETURN_TYPE_MISMATCH
- }
-
- overrideReturnValue(value.toHexString(), true)
-}
-
-/**
- * Overrides all return statements with a constant `Byte` value.
- * All method code is executed the same as unpatched.
- *
- * @see returnEarly
- */
-fun MutableMethod.returnLate(value: Byte) {
- check(returnType.first() == 'B') { RETURN_TYPE_MISMATCH }
- overrideReturnValue(value.toString(), true)
-}
-
-/**
- * Overrides all return statements with a constant `Short` value.
- * All method code is executed the same as unpatched.
- *
- * @see returnEarly
- */
-fun MutableMethod.returnLate(value: Short) {
- check(returnType.first() == 'S') { RETURN_TYPE_MISMATCH }
- overrideReturnValue(value.toString(), true)
-}
-
-/**
- * Overrides all return statements with a constant `Char` value.
- * All method code is executed the same as unpatched.
- *
- * @see returnEarly
- */
-fun MutableMethod.returnLate(value: Char) {
- check(returnType.first() == 'C') { RETURN_TYPE_MISMATCH }
- overrideReturnValue(value.code.toString(), true)
-}
-
-/**
- * Overrides all return statements with a constant `Int` value.
- * All method code is executed the same as unpatched.
- *
- * @see returnEarly
- */
-fun MutableMethod.returnLate(value: Int) {
- check(returnType.first() == 'I') { RETURN_TYPE_MISMATCH }
- overrideReturnValue(value.toString(), true)
-}
-
-/**
- * Overrides all return statements with a constant `Long` value.
- * All method code is executed the same as unpatched.
- *
- * @see returnEarly
- */
-fun MutableMethod.returnLate(value: Long) {
- check(returnType.first() == 'J') { RETURN_TYPE_MISMATCH }
- overrideReturnValue(value.toString(), true)
-}
-
-/**
- * Overrides all return statements with a constant `Float` value.
- * All method code is executed the same as unpatched.
- *
- * @see returnEarly
- */
-fun MutableMethod.returnLate(value: Float) {
- check(returnType.first() == 'F') { RETURN_TYPE_MISMATCH }
- overrideReturnValue(value.toString(), true)
-}
+fun MutableMethod.returnEarly(value: Double) = returnDouble(value, false)
/**
* Overrides all return statements with a constant `Double` value.
@@ -1043,75 +1051,164 @@ fun MutableMethod.returnLate(value: Float) {
*
* @see returnEarly
*/
-fun MutableMethod.returnLate(value: Double) {
- check(returnType.first() == 'D') { RETURN_TYPE_MISMATCH }
- overrideReturnValue(value.toString(), true)
-}
+fun MutableMethod.returnLate(value: Double) = returnDouble(value, true)
-/**
- * Overrides all return statements with a constant String value.
- * All method code is executed the same as unpatched.
- *
- * Target method must have return type
- * Ljava/lang/String; or Ljava/lang/CharSequence;
- *
- * @see returnEarly
- */
-fun MutableMethod.returnLate(value: String) {
- check(returnType == "Ljava/lang/String;" || returnType == "Ljava/lang/CharSequence;") {
- RETURN_TYPE_MISMATCH
- }
- overrideReturnValue(value, true)
-}
-
-private fun MutableMethod.overrideReturnValue(value: String, returnLate: Boolean) {
- val instructions = if (returnType == "Ljava/lang/String;" || returnType == "Ljava/lang/CharSequence;" ) {
- """
- const-string v0, "$value"
- return-object v0
- """
- } else when (returnType.first()) {
- // If return type is an object, always return null.
- 'L', '[' -> {
- """
+private fun MutableMethod.overrideReturnValue(value: EncodedValue?, returnLate: Boolean) {
+ val instructions = if (value == null) {
+ require(!returnLate) {
+ "Cannot return late for method of void type"
+ }
+ "return-void"
+ } else {
+ val encodedValue = DexFormatter.INSTANCE.getEncodedValue(value)
+ when (value) {
+ is NullEncodedValue -> {
+ """
const/4 v0, 0x0
return-object v0
- """
- }
+ """
+ }
- 'V' -> {
- "return-void"
- }
+ is StringEncodedValue -> {
+ """
+ const-string v0, $encodedValue
+ return-object v0
+ """
+ }
- 'B', 'Z' -> {
- """
- const/4 v0, $value
- return v0
- """
- }
+ is ByteEncodedValue -> {
+ if (returnType == "B") {
+ """
+ const/4 v0, $encodedValue
+ return v0
+ """
+ } else {
+ """
+ const/4 v0, $encodedValue
+ invoke-static { v0 }, Ljava/lang/Byte;->valueOf(B)Ljava/lang/Byte;
+ move-result-object v0
+ return-object v0
+ """
+ }
+ }
- 'S', 'C' -> {
- """
- const/16 v0, $value
- return v0
- """
- }
+ is BooleanEncodedValue -> {
+ val encodedValue = value.value.toHexString()
+ if (returnType == "Z") {
+ """
+ const/4 v0, $encodedValue
+ return v0
+ """
+ } else {
+ """
+ const/4 v0, $encodedValue
+ invoke-static { v0 }, Ljava/lang/Boolean;->valueOf(Z)Ljava/lang/Boolean;
+ move-result-object v0
+ return-object v0
+ """
+ }
+ }
- 'I', 'F' -> {
- """
- const v0, $value
- return v0
- """
- }
+ is ShortEncodedValue -> {
+ if (returnType == "S") {
+ """
+ const/16 v0, $encodedValue
+ return v0
+ """
+ } else {
+ """
+ const/16 v0, $encodedValue
+ invoke-static { v0 }, Ljava/lang/Short;->valueOf(S)Ljava/lang/Short;
+ move-result-object v0
+ return-object v0
+ """
+ }
+ }
- 'J', 'D' -> {
- """
- const-wide v0, $value
- return-wide v0
- """
- }
+ is CharEncodedValue -> {
+ if (returnType == "C") {
+ """
+ const/16 v0, $encodedValue
+ return v0
+ """
+ } else {
+ """
+ const/16 v0, $encodedValue
+ invoke-static { v0 }, Ljava/lang/Character;->valueOf(C)Ljava/lang/Character;
+ move-result-object v0
+ return-object v0
+ """
+ }
+ }
- else -> throw Exception("Return type is not supported: $this")
+ is IntEncodedValue -> {
+ if (returnType == "I") {
+ """
+ const v0, $encodedValue
+ return v0
+ """
+ } else {
+ """
+ const v0, $encodedValue
+ invoke-static { v0 }, Ljava/lang/Integer;->valueOf(I)Ljava/lang/Integer;
+ move-result-object v0
+ return-object v0
+ """
+ }
+ }
+
+ is FloatEncodedValue -> {
+ val encodedValue = "${encodedValue}f"
+ if (returnType == "F") {
+ """
+ const v0, $encodedValue
+ return v0
+ """
+ } else {
+ """
+ const v0, $encodedValue
+ invoke-static { v0 }, Ljava/lang/Float;->valueOf(F)Ljava/lang/Float;
+ move-result-object v0
+ return-object v0
+ """
+ }
+ }
+
+ is LongEncodedValue -> {
+ val encodedValue = "${encodedValue}L"
+ if (returnType == "J") {
+ """
+ const-wide v0, $encodedValue
+ return-wide v0
+ """
+ } else {
+ """
+ const-wide v0, $encodedValue
+ invoke-static { v0 }, Ljava/lang/Long;->valueOf(J)Ljava/lang/Long;
+ move-result-object v0
+ return-object v0
+ """
+ }
+ }
+
+ is DoubleEncodedValue -> {
+ if (returnType == "D") {
+ """
+ const-wide v0, $encodedValue
+ return-wide v0
+ """
+ } else {
+ """
+ const-wide v0, $encodedValue
+ invoke-static { v0 }, Ljava/lang/Double;->valueOf(D)Ljava/lang/Double;
+ move-result-object v0
+ return-object v0
+ """
+ }
+ }
+
+ else -> throw IllegalArgumentException("Value $value cannot be returned from $this")
+ }
}
if (returnLate) {
diff --git a/patches/src/main/kotlin/app/revanced/util/Utils.kt b/patches/src/main/kotlin/app/revanced/util/Utils.kt
index ef7d0ef1a..6305809ad 100644
--- a/patches/src/main/kotlin/app/revanced/util/Utils.kt
+++ b/patches/src/main/kotlin/app/revanced/util/Utils.kt
@@ -7,4 +7,21 @@ internal object Utils {
.trimIndent() // Remove the leading newline.
}
-internal fun Boolean.toHexString(): String = if (this) "0x1" else "0x0"
\ No newline at end of file
+internal fun Boolean.toHexString(): String = if (this) "0x1" else "0x0"
+
+internal fun Class<*>.allAssignableTypes(): Set> {
+ val result = mutableSetOf>()
+
+ fun visit(child: Class<*>?) {
+ if (child == null || !result.add(child)) {
+ return
+ }
+
+ child.interfaces.forEach(::visit)
+ visit(child.superclass)
+ }
+
+ visit(this)
+
+ return result
+}
From f5cbb31724d15f7e939b96ee0186fd0a108f9fdc Mon Sep 17 00:00:00 2001
From: xehpuk
Date: Sun, 4 Jan 2026 03:38:04 +0100
Subject: [PATCH 18/42] feat(Strava): Add `Disable Quick Edit` patch (#6452)
Co-authored-by: oSumAtrIX
---
patches/api/patches.api | 4 ++++
.../strava/quickedit/DisableQuickEditPatch.kt | 16 ++++++++++++++++
.../patches/strava/quickedit/Fingerprints.kt | 10 ++++++++++
3 files changed, 30 insertions(+)
create mode 100644 patches/src/main/kotlin/app/revanced/patches/strava/quickedit/DisableQuickEditPatch.kt
create mode 100644 patches/src/main/kotlin/app/revanced/patches/strava/quickedit/Fingerprints.kt
diff --git a/patches/api/patches.api b/patches/api/patches.api
index 63a56070a..439040929 100644
--- a/patches/api/patches.api
+++ b/patches/api/patches.api
@@ -1200,6 +1200,10 @@ public final class app/revanced/patches/strava/privacy/BlockSnowplowTrackingPatc
public static final fun getBlockSnowplowTrackingPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
+public final class app/revanced/patches/strava/quickedit/DisableQuickEditPatchKt {
+ public static final fun getDisableQuickEditPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
+}
+
public final class app/revanced/patches/strava/subscription/UnlockSubscriptionPatchKt {
public static final fun getUnlockSubscriptionPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
diff --git a/patches/src/main/kotlin/app/revanced/patches/strava/quickedit/DisableQuickEditPatch.kt b/patches/src/main/kotlin/app/revanced/patches/strava/quickedit/DisableQuickEditPatch.kt
new file mode 100644
index 000000000..128f86870
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/strava/quickedit/DisableQuickEditPatch.kt
@@ -0,0 +1,16 @@
+package app.revanced.patches.strava.quickedit
+
+import app.revanced.patcher.patch.bytecodePatch
+import app.revanced.util.returnEarly
+
+@Suppress("unused")
+val disableQuickEditPatch = bytecodePatch(
+ name = "Disable Quick Edit",
+ description = "Prevents the Quick Edit prompt from popping up.",
+) {
+ compatibleWith("com.strava")
+
+ execute {
+ getHasAccessToQuickEditFingerprint.method.returnEarly()
+ }
+}
diff --git a/patches/src/main/kotlin/app/revanced/patches/strava/quickedit/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/strava/quickedit/Fingerprints.kt
new file mode 100644
index 000000000..acd48542b
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/strava/quickedit/Fingerprints.kt
@@ -0,0 +1,10 @@
+package app.revanced.patches.strava.quickedit
+
+import app.revanced.patcher.fingerprint
+
+internal val getHasAccessToQuickEditFingerprint = fingerprint {
+ returns("Z")
+ custom { method, _ ->
+ method.name == "getHasAccessToQuickEdit"
+ }
+}
From 1cc2cb9cb28dd1b79529ee1d917221381b55d916 Mon Sep 17 00:00:00 2001
From: semantic-release-bot
Date: Sun, 4 Jan 2026 02:43:29 +0000
Subject: [PATCH 19/42] chore: Release v5.48.0-dev.7 [skip ci]
# [5.48.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.6...v5.48.0-dev.7) (2026-01-04)
### Features
* **Strava:** Add `Disable Quick Edit` patch ([#6452](https://github.com/ReVanced/revanced-patches/issues/6452)) ([f5cbb31](https://github.com/ReVanced/revanced-patches/commit/f5cbb31724d15f7e939b96ee0186fd0a108f9fdc))
* **Strava:** Add `Overwrite media upload parameters` patch ([#6410](https://github.com/ReVanced/revanced-patches/issues/6410)) ([b42ae27](https://github.com/ReVanced/revanced-patches/commit/b42ae27ce66ebad9e9cfc5b70fc121df5bad7567))
---
CHANGELOG.md | 8 ++++++++
gradle.properties | 2 +-
2 files changed, 9 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cd5f243ea..5d37e7769 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,11 @@
+# [5.48.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.6...v5.48.0-dev.7) (2026-01-04)
+
+
+### Features
+
+* **Strava:** Add `Disable Quick Edit` patch ([#6452](https://github.com/ReVanced/revanced-patches/issues/6452)) ([f5cbb31](https://github.com/ReVanced/revanced-patches/commit/f5cbb31724d15f7e939b96ee0186fd0a108f9fdc))
+* **Strava:** Add `Overwrite media upload parameters` patch ([#6410](https://github.com/ReVanced/revanced-patches/issues/6410)) ([b42ae27](https://github.com/ReVanced/revanced-patches/commit/b42ae27ce66ebad9e9cfc5b70fc121df5bad7567))
+
# [5.48.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.5...v5.48.0-dev.6) (2026-01-04)
diff --git a/gradle.properties b/gradle.properties
index 38f8c15ff..aa4a96d3a 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -3,4 +3,4 @@ org.gradle.jvmargs = -Xms512M -Xmx2048M
org.gradle.parallel = true
android.useAndroidX = true
kotlin.code.style = official
-version = 5.48.0-dev.6
+version = 5.48.0-dev.7
From d25dcfe49ac331c9b3dca739ba0be95dbab669cc Mon Sep 17 00:00:00 2001
From: Swakshan <56347042+Swakshan@users.noreply.github.com>
Date: Sun, 4 Jan 2026 18:44:29 +0530
Subject: [PATCH 20/42] feat(Letterboxd): Add `Unlock app icons` patch (#6415)
---
patches/api/patches.api | 4 ++++
.../unlock/unlockAppIcons/Fingerprints.kt | 9 +++++++++
.../unlock/unlockAppIcons/UnlockAppIconsPatch.kt | 16 ++++++++++++++++
3 files changed, 29 insertions(+)
create mode 100644 patches/src/main/kotlin/app/revanced/patches/letterboxd/unlock/unlockAppIcons/Fingerprints.kt
create mode 100644 patches/src/main/kotlin/app/revanced/patches/letterboxd/unlock/unlockAppIcons/UnlockAppIconsPatch.kt
diff --git a/patches/api/patches.api b/patches/api/patches.api
index 439040929..9b5ac4d0c 100644
--- a/patches/api/patches.api
+++ b/patches/api/patches.api
@@ -352,6 +352,10 @@ public final class app/revanced/patches/letterboxd/ads/HideAdsPatchKt {
public static final fun getHideAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
+public final class app/revanced/patches/letterboxd/unlock/unlockAppIcons/UnlockAppIconsPatchKt {
+ public static final fun getUnlockAppIconsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
+}
+
public final class app/revanced/patches/lightroom/misc/login/DisableMandatoryLoginPatchKt {
public static final fun getDisableMandatoryLoginPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
diff --git a/patches/src/main/kotlin/app/revanced/patches/letterboxd/unlock/unlockAppIcons/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/letterboxd/unlock/unlockAppIcons/Fingerprints.kt
new file mode 100644
index 000000000..1b549cd57
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/letterboxd/unlock/unlockAppIcons/Fingerprints.kt
@@ -0,0 +1,9 @@
+package app.revanced.patches.letterboxd.unlock.unlockAppIcons
+
+import app.revanced.patcher.fingerprint
+
+internal val getCanChangeAppIconFingerprint = fingerprint {
+ custom { method, classDef ->
+ method.name == "getCanChangeAppIcon" && classDef.type.endsWith("SettingsAppIconFragment;")
+ }
+}
diff --git a/patches/src/main/kotlin/app/revanced/patches/letterboxd/unlock/unlockAppIcons/UnlockAppIconsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/letterboxd/unlock/unlockAppIcons/UnlockAppIconsPatch.kt
new file mode 100644
index 000000000..54d6f3df9
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/letterboxd/unlock/unlockAppIcons/UnlockAppIconsPatch.kt
@@ -0,0 +1,16 @@
+
+package app.revanced.patches.letterboxd.unlock.unlockAppIcons
+
+import app.revanced.patcher.patch.bytecodePatch
+import app.revanced.util.returnEarly
+
+@Suppress("unused")
+val unlockAppIconsPatch = bytecodePatch(
+ name = "Unlock app icons",
+) {
+ compatibleWith("com.letterboxd.letterboxd")
+
+ execute {
+ getCanChangeAppIconFingerprint.method.returnEarly(true)
+ }
+}
From a3f7609fe38033975d7c52e63e14b8b09902529f Mon Sep 17 00:00:00 2001
From: semantic-release-bot
Date: Sun, 4 Jan 2026 13:17:54 +0000
Subject: [PATCH 21/42] chore: Release v5.48.0-dev.8 [skip ci]
# [5.48.0-dev.8](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.7...v5.48.0-dev.8) (2026-01-04)
### Features
* **Letterboxd:** Add `Unlock app icons` patch ([#6415](https://github.com/ReVanced/revanced-patches/issues/6415)) ([d25dcfe](https://github.com/ReVanced/revanced-patches/commit/d25dcfe49ac331c9b3dca739ba0be95dbab669cc))
---
CHANGELOG.md | 7 +++++++
gradle.properties | 2 +-
2 files changed, 8 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5d37e7769..d9f397d0c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,10 @@
+# [5.48.0-dev.8](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.7...v5.48.0-dev.8) (2026-01-04)
+
+
+### Features
+
+* **Letterboxd:** Add `Unlock app icons` patch ([#6415](https://github.com/ReVanced/revanced-patches/issues/6415)) ([d25dcfe](https://github.com/ReVanced/revanced-patches/commit/d25dcfe49ac331c9b3dca739ba0be95dbab669cc))
+
# [5.48.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.6...v5.48.0-dev.7) (2026-01-04)
diff --git a/gradle.properties b/gradle.properties
index aa4a96d3a..5487ca1f0 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -3,4 +3,4 @@ org.gradle.jvmargs = -Xms512M -Xmx2048M
org.gradle.parallel = true
android.useAndroidX = true
kotlin.code.style = official
-version = 5.48.0-dev.7
+version = 5.48.0-dev.8
From 3d754575a4db90a2138b5deedf24f70bc72ddaf7 Mon Sep 17 00:00:00 2001
From: Ushie
Date: Thu, 8 Jan 2026 00:31:15 +0300
Subject: [PATCH 22/42] ci: Simplify Crowdin translation file destination path
(#6463)
---
crowdin.yml | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/crowdin.yml b/crowdin.yml
index 148f321cd..81022c88c 100644
--- a/crowdin.yml
+++ b/crowdin.yml
@@ -1,8 +1,9 @@
project_id_env: "CROWDIN_PROJECT_ID"
api_token_env: "CROWDIN_PERSONAL_TOKEN"
-preserve_hierarchy: false
+preserve_hierarchy: true
files:
- source: patches/src/main/resources/addresources/values/strings.xml
+ dest: patches.xml
translation: patches/src/main/resources/addresources/values-%android_code%/strings.xml
skip_untranslated_strings: true
From 6312fe8d60da24465c0c1b0fa4e94ceb79873d9c Mon Sep 17 00:00:00 2001
From: 1fexd <58902674+1fexd@users.noreply.github.com>
Date: Thu, 8 Jan 2026 00:06:05 +0000
Subject: [PATCH 23/42] feat: Disable Play Integrity patch (#6412)
Co-authored-by: oSumAtrIX
---
.../disable-play-integrity/build.gradle.kts | 20 ++++++
.../src/main/AndroidManifest.xml | 1 +
.../protocol/IExpressIntegrityService.aidl | 8 +++
.../IExpressIntegrityServiceCallback.aidl | 5 ++
.../integrity/protocol/IIntegrityService.aidl | 8 +++
.../protocol/IIntegrityServiceCallback.aidl | 7 +++
.../src/main/java/android/ext/PackageId.java | 10 +++
.../main/java/android/os/BinderWrapper.java | 62 +++++++++++++++++++
.../ClassicPlayIntegrityServiceWrapper.java | 41 ++++++++++++
.../PlayIntegrityServiceWrapper.java | 48 ++++++++++++++
.../lib/playintegrity/PlayIntegrityUtils.java | 35 +++++++++++
.../StandardPlayIntegrityServiceWrapper.java | 42 +++++++++++++
.../lib/util/ServiceConnectionWrapper.java | 49 +++++++++++++++
.../DisablePlayIntegrityPatch.java | 17 +++++
.../internal/os/FakeBackgroundHandler.java | 11 ++++
patches/api/patches.api | 4 ++
.../playintegrity/DisablePlayIntegrity.kt | 55 ++++++++++++++++
17 files changed, 423 insertions(+)
create mode 100644 extensions/all/misc/disable-play-integrity/build.gradle.kts
create mode 100644 extensions/all/misc/disable-play-integrity/src/main/AndroidManifest.xml
create mode 100644 extensions/all/misc/disable-play-integrity/src/main/aidl/com/google/android/play/core/integrity/protocol/IExpressIntegrityService.aidl
create mode 100644 extensions/all/misc/disable-play-integrity/src/main/aidl/com/google/android/play/core/integrity/protocol/IExpressIntegrityServiceCallback.aidl
create mode 100644 extensions/all/misc/disable-play-integrity/src/main/aidl/com/google/android/play/core/integrity/protocol/IIntegrityService.aidl
create mode 100644 extensions/all/misc/disable-play-integrity/src/main/aidl/com/google/android/play/core/integrity/protocol/IIntegrityServiceCallback.aidl
create mode 100644 extensions/all/misc/disable-play-integrity/src/main/java/android/ext/PackageId.java
create mode 100644 extensions/all/misc/disable-play-integrity/src/main/java/android/os/BinderWrapper.java
create mode 100644 extensions/all/misc/disable-play-integrity/src/main/java/app/grapheneos/gmscompat/lib/playintegrity/ClassicPlayIntegrityServiceWrapper.java
create mode 100644 extensions/all/misc/disable-play-integrity/src/main/java/app/grapheneos/gmscompat/lib/playintegrity/PlayIntegrityServiceWrapper.java
create mode 100644 extensions/all/misc/disable-play-integrity/src/main/java/app/grapheneos/gmscompat/lib/playintegrity/PlayIntegrityUtils.java
create mode 100644 extensions/all/misc/disable-play-integrity/src/main/java/app/grapheneos/gmscompat/lib/playintegrity/StandardPlayIntegrityServiceWrapper.java
create mode 100644 extensions/all/misc/disable-play-integrity/src/main/java/app/grapheneos/gmscompat/lib/util/ServiceConnectionWrapper.java
create mode 100644 extensions/all/misc/disable-play-integrity/src/main/java/app/revanced/extension/playintegrity/DisablePlayIntegrityPatch.java
create mode 100644 extensions/all/misc/disable-play-integrity/src/main/java/com/android/internal/os/FakeBackgroundHandler.java
create mode 100644 patches/src/main/kotlin/app/revanced/patches/all/misc/playintegrity/DisablePlayIntegrity.kt
diff --git a/extensions/all/misc/disable-play-integrity/build.gradle.kts b/extensions/all/misc/disable-play-integrity/build.gradle.kts
new file mode 100644
index 000000000..549297227
--- /dev/null
+++ b/extensions/all/misc/disable-play-integrity/build.gradle.kts
@@ -0,0 +1,20 @@
+android {
+ namespace = "app.revanced.extension"
+
+ defaultConfig {
+ minSdk = 21
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_11
+ targetCompatibility = JavaVersion.VERSION_11
+ }
+
+ buildFeatures {
+ aidl = true
+ }
+}
+
+dependencies {
+ compileOnly(libs.annotation)
+}
diff --git a/extensions/all/misc/disable-play-integrity/src/main/AndroidManifest.xml b/extensions/all/misc/disable-play-integrity/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..9b65eb06c
--- /dev/null
+++ b/extensions/all/misc/disable-play-integrity/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+
diff --git a/extensions/all/misc/disable-play-integrity/src/main/aidl/com/google/android/play/core/integrity/protocol/IExpressIntegrityService.aidl b/extensions/all/misc/disable-play-integrity/src/main/aidl/com/google/android/play/core/integrity/protocol/IExpressIntegrityService.aidl
new file mode 100644
index 000000000..7b8f59f1d
--- /dev/null
+++ b/extensions/all/misc/disable-play-integrity/src/main/aidl/com/google/android/play/core/integrity/protocol/IExpressIntegrityService.aidl
@@ -0,0 +1,8 @@
+package com.google.android.play.core.integrity.protocol;
+
+import android.os.Bundle;
+import com.google.android.play.core.integrity.protocol.IExpressIntegrityServiceCallback;
+
+interface IExpressIntegrityService {
+ oneway void requestIntegrityToken(in Bundle request, IExpressIntegrityServiceCallback callback) = 2;
+}
diff --git a/extensions/all/misc/disable-play-integrity/src/main/aidl/com/google/android/play/core/integrity/protocol/IExpressIntegrityServiceCallback.aidl b/extensions/all/misc/disable-play-integrity/src/main/aidl/com/google/android/play/core/integrity/protocol/IExpressIntegrityServiceCallback.aidl
new file mode 100644
index 000000000..624167afb
--- /dev/null
+++ b/extensions/all/misc/disable-play-integrity/src/main/aidl/com/google/android/play/core/integrity/protocol/IExpressIntegrityServiceCallback.aidl
@@ -0,0 +1,5 @@
+package com.google.android.play.core.integrity.protocol;
+
+interface IExpressIntegrityServiceCallback {
+ oneway void onRequestExpressIntegrityTokenResult(in Bundle result) = 2;
+}
diff --git a/extensions/all/misc/disable-play-integrity/src/main/aidl/com/google/android/play/core/integrity/protocol/IIntegrityService.aidl b/extensions/all/misc/disable-play-integrity/src/main/aidl/com/google/android/play/core/integrity/protocol/IIntegrityService.aidl
new file mode 100644
index 000000000..bb1bcd551
--- /dev/null
+++ b/extensions/all/misc/disable-play-integrity/src/main/aidl/com/google/android/play/core/integrity/protocol/IIntegrityService.aidl
@@ -0,0 +1,8 @@
+package com.google.android.play.core.integrity.protocol;
+
+import android.os.Bundle;
+import com.google.android.play.core.integrity.protocol.IIntegrityServiceCallback;
+
+interface IIntegrityService {
+ oneway void requestIntegrityToken(in Bundle request, IIntegrityServiceCallback callback) = 1;
+}
diff --git a/extensions/all/misc/disable-play-integrity/src/main/aidl/com/google/android/play/core/integrity/protocol/IIntegrityServiceCallback.aidl b/extensions/all/misc/disable-play-integrity/src/main/aidl/com/google/android/play/core/integrity/protocol/IIntegrityServiceCallback.aidl
new file mode 100644
index 000000000..9485ec169
--- /dev/null
+++ b/extensions/all/misc/disable-play-integrity/src/main/aidl/com/google/android/play/core/integrity/protocol/IIntegrityServiceCallback.aidl
@@ -0,0 +1,7 @@
+package com.google.android.play.core.integrity.protocol;
+
+import android.os.Bundle;
+
+interface IIntegrityServiceCallback {
+ oneway void onResult(in Bundle result) = 1;
+}
diff --git a/extensions/all/misc/disable-play-integrity/src/main/java/android/ext/PackageId.java b/extensions/all/misc/disable-play-integrity/src/main/java/android/ext/PackageId.java
new file mode 100644
index 000000000..31c2ca6db
--- /dev/null
+++ b/extensions/all/misc/disable-play-integrity/src/main/java/android/ext/PackageId.java
@@ -0,0 +1,10 @@
+package android.ext;
+/** @hide */
+// Int values that are assigned to packages in this interface can be retrieved at runtime from
+// ApplicationInfo.ext().getPackageId() or from AndroidPackage.ext().getPackageId() (in system_server).
+//
+// PackageIds are assigned to parsed APKs only after they are verified, either by a certificate check
+// or by a check that the APK is stored on an immutable OS partition.
+public interface PackageId {
+ String PLAY_STORE_NAME = "com.android.vending";
+}
diff --git a/extensions/all/misc/disable-play-integrity/src/main/java/android/os/BinderWrapper.java b/extensions/all/misc/disable-play-integrity/src/main/java/android/os/BinderWrapper.java
new file mode 100644
index 000000000..a01806441
--- /dev/null
+++ b/extensions/all/misc/disable-play-integrity/src/main/java/android/os/BinderWrapper.java
@@ -0,0 +1,62 @@
+package android.os;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.io.FileDescriptor;
+
+/** @hide */
+public class BinderWrapper implements IBinder {
+ protected final IBinder base;
+
+ public BinderWrapper(IBinder base) {
+ this.base = base;
+ }
+
+ @Override
+ public boolean transact(int code, @NonNull Parcel data, @Nullable Parcel reply, int flags) throws RemoteException {
+ return base.transact(code, data, reply, flags);
+ }
+
+ @Nullable
+ @Override
+ public IInterface queryLocalInterface(@NonNull String descriptor) {
+ return base.queryLocalInterface(descriptor);
+ }
+
+ @Nullable
+ @Override
+ public String getInterfaceDescriptor() throws RemoteException {
+ return base.getInterfaceDescriptor();
+ }
+
+ @Override
+ public boolean pingBinder() {
+ return base.pingBinder();
+ }
+
+ @Override
+ public boolean isBinderAlive() {
+ return base.isBinderAlive();
+ }
+
+ @Override
+ public void dump(@NonNull FileDescriptor fd, @Nullable String[] args) throws RemoteException {
+ base.dump(fd, args);
+ }
+
+ @Override
+ public void dumpAsync(@NonNull FileDescriptor fd, @Nullable String[] args) throws RemoteException {
+ base.dumpAsync(fd, args);
+ }
+
+ @Override
+ public void linkToDeath(@NonNull DeathRecipient recipient, int flags) throws RemoteException {
+ base.linkToDeath(recipient, flags);
+ }
+
+ @Override
+ public boolean unlinkToDeath(@NonNull DeathRecipient recipient, int flags) {
+ return base.unlinkToDeath(recipient, flags);
+ }
+}
diff --git a/extensions/all/misc/disable-play-integrity/src/main/java/app/grapheneos/gmscompat/lib/playintegrity/ClassicPlayIntegrityServiceWrapper.java b/extensions/all/misc/disable-play-integrity/src/main/java/app/grapheneos/gmscompat/lib/playintegrity/ClassicPlayIntegrityServiceWrapper.java
new file mode 100644
index 000000000..3bd88d2a6
--- /dev/null
+++ b/extensions/all/misc/disable-play-integrity/src/main/java/app/grapheneos/gmscompat/lib/playintegrity/ClassicPlayIntegrityServiceWrapper.java
@@ -0,0 +1,41 @@
+package app.grapheneos.gmscompat.lib.playintegrity;
+
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.internal.os.FakeBackgroundHandler;
+import com.google.android.play.core.integrity.protocol.IIntegrityService;
+import com.google.android.play.core.integrity.protocol.IIntegrityServiceCallback;
+
+class ClassicPlayIntegrityServiceWrapper extends PlayIntegrityServiceWrapper {
+
+ ClassicPlayIntegrityServiceWrapper(IBinder base) {
+ super(base);
+ requestIntegrityTokenTxnCode = 2; // IIntegrityService.Stub.TRANSACTION_requestIntegrityToken
+ }
+
+ static class TokenRequestStub extends IIntegrityService.Stub {
+ public void requestIntegrityToken(Bundle request, IIntegrityServiceCallback callback) {
+ Runnable r = () -> {
+ var result = new Bundle();
+ // https://developer.android.com/google/play/integrity/reference/com/google/android/play/core/integrity/model/IntegrityErrorCode.html#API_NOT_AVAILABLE
+ final int API_NOT_AVAILABLE = -1;
+ result.putInt("error", API_NOT_AVAILABLE);
+ try {
+ callback.onResult(result);
+ } catch (RemoteException e) {
+ Log.e("IIntegrityService.Stub", "", e);
+ }
+ };
+ FakeBackgroundHandler.getHandler().postDelayed(r, getTokenRequestResultDelay());
+ }
+ };
+
+ @Override
+ protected Binder createTokenRequestStub() {
+ return new TokenRequestStub();
+ }
+}
diff --git a/extensions/all/misc/disable-play-integrity/src/main/java/app/grapheneos/gmscompat/lib/playintegrity/PlayIntegrityServiceWrapper.java b/extensions/all/misc/disable-play-integrity/src/main/java/app/grapheneos/gmscompat/lib/playintegrity/PlayIntegrityServiceWrapper.java
new file mode 100644
index 000000000..0418b4fe7
--- /dev/null
+++ b/extensions/all/misc/disable-play-integrity/src/main/java/app/grapheneos/gmscompat/lib/playintegrity/PlayIntegrityServiceWrapper.java
@@ -0,0 +1,48 @@
+package app.grapheneos.gmscompat.lib.playintegrity;
+
+import android.os.Binder;
+import android.os.BinderWrapper;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.RemoteException;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+
+abstract class PlayIntegrityServiceWrapper extends BinderWrapper {
+ final String TAG;
+ protected int requestIntegrityTokenTxnCode;
+
+ public PlayIntegrityServiceWrapper(IBinder base) {
+ super(base);
+ TAG = getClass().getSimpleName();
+ }
+
+ protected abstract Binder createTokenRequestStub();
+
+ @Override
+ public boolean transact(int code, Parcel data, @Nullable Parcel reply, int flags) throws RemoteException {
+ if (code == requestIntegrityTokenTxnCode) {
+ if (maybeStubOutIntegrityTokenRequest(code, data, reply, flags)) {
+ return true;
+ }
+ }
+ return super.transact(code, data, reply, flags);
+ }
+
+ private boolean maybeStubOutIntegrityTokenRequest(int code, Parcel data, @Nullable Parcel reply, int flags) {
+ Log.d(TAG, "integrity token request detected");
+
+ try {
+ createTokenRequestStub().transact(code, data, reply, flags);
+ } catch (RemoteException e) {
+ // this is a local call
+ throw new IllegalStateException(e);
+ }
+ return true;
+ }
+
+ protected static long getTokenRequestResultDelay() {
+ return 500L;
+ }
+}
diff --git a/extensions/all/misc/disable-play-integrity/src/main/java/app/grapheneos/gmscompat/lib/playintegrity/PlayIntegrityUtils.java b/extensions/all/misc/disable-play-integrity/src/main/java/app/grapheneos/gmscompat/lib/playintegrity/PlayIntegrityUtils.java
new file mode 100644
index 000000000..6ff4720cc
--- /dev/null
+++ b/extensions/all/misc/disable-play-integrity/src/main/java/app/grapheneos/gmscompat/lib/playintegrity/PlayIntegrityUtils.java
@@ -0,0 +1,35 @@
+package app.grapheneos.gmscompat.lib.playintegrity;
+
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.ext.PackageId;
+import android.os.IBinder;
+import androidx.annotation.Nullable;
+import app.grapheneos.gmscompat.lib.util.ServiceConnectionWrapper;
+import java.util.function.UnaryOperator;
+
+public class PlayIntegrityUtils {
+
+ public static @Nullable ServiceConnection maybeReplaceServiceConnection(Intent service, ServiceConnection orig) {
+ if (PackageId.PLAY_STORE_NAME.equals(service.getPackage())) {
+ UnaryOperator binderOverride = null;
+
+ final String CLASSIC_SERVICE =
+ "com.google.android.play.core.integrityservice.BIND_INTEGRITY_SERVICE";
+ final String STANDARD_SERVICE =
+ "com.google.android.play.core.expressintegrityservice.BIND_EXPRESS_INTEGRITY_SERVICE";
+
+ String action = service.getAction();
+ if (STANDARD_SERVICE.equals(action)) {
+ binderOverride = StandardPlayIntegrityServiceWrapper::new;
+ } else if (CLASSIC_SERVICE.equals(action)) {
+ binderOverride = ClassicPlayIntegrityServiceWrapper::new;
+ }
+
+ if (binderOverride != null) {
+ return new ServiceConnectionWrapper(orig, binderOverride);
+ }
+ }
+ return null;
+ }
+}
diff --git a/extensions/all/misc/disable-play-integrity/src/main/java/app/grapheneos/gmscompat/lib/playintegrity/StandardPlayIntegrityServiceWrapper.java b/extensions/all/misc/disable-play-integrity/src/main/java/app/grapheneos/gmscompat/lib/playintegrity/StandardPlayIntegrityServiceWrapper.java
new file mode 100644
index 000000000..c1c4937f0
--- /dev/null
+++ b/extensions/all/misc/disable-play-integrity/src/main/java/app/grapheneos/gmscompat/lib/playintegrity/StandardPlayIntegrityServiceWrapper.java
@@ -0,0 +1,42 @@
+package app.grapheneos.gmscompat.lib.playintegrity;
+
+import android.annotation.SuppressLint;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+import com.android.internal.os.FakeBackgroundHandler;
+import com.google.android.play.core.integrity.protocol.IExpressIntegrityService;
+import com.google.android.play.core.integrity.protocol.IExpressIntegrityServiceCallback;
+
+@SuppressLint("LongLogTag")
+class StandardPlayIntegrityServiceWrapper extends PlayIntegrityServiceWrapper {
+
+ StandardPlayIntegrityServiceWrapper(IBinder base) {
+ super(base);
+ requestIntegrityTokenTxnCode = 3; // IExpressIntegrityService.Stub.TRANSACTION_requestIntegrityToken
+ }
+
+ static class TokenRequestStub extends IExpressIntegrityService.Stub {
+ public void requestIntegrityToken(Bundle request, IExpressIntegrityServiceCallback callback) {
+ Runnable r = () -> {
+ var result = new Bundle();
+ // https://developer.android.com/google/play/integrity/reference/com/google/android/play/core/integrity/model/StandardIntegrityErrorCode.html#API_NOT_AVAILABLE
+ final int API_NOT_AVAILABLE = -1;
+ result.putInt("error", API_NOT_AVAILABLE);
+ try {
+ callback.onRequestExpressIntegrityTokenResult(result);
+ } catch (RemoteException e) {
+ Log.e("IExpressIntegrityService.Stub", "", e);
+ }
+ };
+ FakeBackgroundHandler.getHandler().postDelayed(r, getTokenRequestResultDelay());
+ }
+ };
+
+ @Override
+ protected Binder createTokenRequestStub() {
+ return new TokenRequestStub();
+ }
+}
diff --git a/extensions/all/misc/disable-play-integrity/src/main/java/app/grapheneos/gmscompat/lib/util/ServiceConnectionWrapper.java b/extensions/all/misc/disable-play-integrity/src/main/java/app/grapheneos/gmscompat/lib/util/ServiceConnectionWrapper.java
new file mode 100644
index 000000000..9edfc39f8
--- /dev/null
+++ b/extensions/all/misc/disable-play-integrity/src/main/java/app/grapheneos/gmscompat/lib/util/ServiceConnectionWrapper.java
@@ -0,0 +1,49 @@
+package app.grapheneos.gmscompat.lib.util;
+
+import android.content.ComponentName;
+import android.content.ServiceConnection;
+import android.os.Build;
+import android.os.IBinder;
+
+import java.util.function.UnaryOperator;
+
+public class ServiceConnectionWrapper implements ServiceConnection {
+ private final ServiceConnection base;
+ private final UnaryOperator binderOverride;
+
+ public ServiceConnectionWrapper(ServiceConnection base, UnaryOperator binderOverride) {
+ this.base = base;
+ this.binderOverride = binderOverride;
+ }
+
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ IBinder override = binderOverride.apply(service);
+ if (override != null) {
+ service = override;
+ }
+ }
+
+ base.onServiceConnected(name, service);
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ base.onServiceDisconnected(name);
+ }
+
+ @Override
+ public void onBindingDied(ComponentName name) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ base.onBindingDied(name);
+ }
+ }
+
+ @Override
+ public void onNullBinding(ComponentName name) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ base.onNullBinding(name);
+ }
+ }
+}
diff --git a/extensions/all/misc/disable-play-integrity/src/main/java/app/revanced/extension/playintegrity/DisablePlayIntegrityPatch.java b/extensions/all/misc/disable-play-integrity/src/main/java/app/revanced/extension/playintegrity/DisablePlayIntegrityPatch.java
new file mode 100644
index 000000000..a27e56be9
--- /dev/null
+++ b/extensions/all/misc/disable-play-integrity/src/main/java/app/revanced/extension/playintegrity/DisablePlayIntegrityPatch.java
@@ -0,0 +1,17 @@
+package app.revanced.extension.playintegrity;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import app.grapheneos.gmscompat.lib.playintegrity.PlayIntegrityUtils;
+
+public class DisablePlayIntegrityPatch {
+ public static boolean bindService(Context context, Intent service, ServiceConnection conn, int flags) {
+ ServiceConnection override = PlayIntegrityUtils.maybeReplaceServiceConnection(service, conn);
+ if (override != null) {
+ conn = override;
+ }
+
+ return context.bindService(service, conn, flags);
+ }
+}
diff --git a/extensions/all/misc/disable-play-integrity/src/main/java/com/android/internal/os/FakeBackgroundHandler.java b/extensions/all/misc/disable-play-integrity/src/main/java/com/android/internal/os/FakeBackgroundHandler.java
new file mode 100644
index 000000000..6b4cb92b4
--- /dev/null
+++ b/extensions/all/misc/disable-play-integrity/src/main/java/com/android/internal/os/FakeBackgroundHandler.java
@@ -0,0 +1,11 @@
+package com.android.internal.os;
+
+import android.os.Handler;
+import android.os.Looper;
+
+public class FakeBackgroundHandler {
+
+ public static Handler getHandler() {
+ return new Handler(Looper.getMainLooper());
+ }
+}
diff --git a/patches/api/patches.api b/patches/api/patches.api
index 9b5ac4d0c..ec75df2b3 100644
--- a/patches/api/patches.api
+++ b/patches/api/patches.api
@@ -104,6 +104,10 @@ public final class app/revanced/patches/all/misc/packagename/ChangePackageNamePa
public static final fun setPackageNameOption (Lapp/revanced/patcher/patch/Option;)V
}
+public final class app/revanced/patches/all/misc/playintegrity/DisablePlayIntegrityKt {
+ public static final fun getDisablePlayIntegrityPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
+}
+
public final class app/revanced/patches/all/misc/resources/AddResourcesPatchKt {
public static final fun addResource (Ljava/lang/String;Lapp/revanced/util/resource/BaseResource;)Z
public static final fun addResources (Lapp/revanced/patcher/patch/Patch;Lkotlin/jvm/functions/Function1;)Z
diff --git a/patches/src/main/kotlin/app/revanced/patches/all/misc/playintegrity/DisablePlayIntegrity.kt b/patches/src/main/kotlin/app/revanced/patches/all/misc/playintegrity/DisablePlayIntegrity.kt
new file mode 100644
index 000000000..25a948e34
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/all/misc/playintegrity/DisablePlayIntegrity.kt
@@ -0,0 +1,55 @@
+package app.revanced.patches.all.misc.playintegrity
+
+import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
+import app.revanced.patcher.patch.bytecodePatch
+import app.revanced.patches.all.misc.transformation.transformInstructionsPatch
+import app.revanced.util.getReference
+import com.android.tools.smali.dexlib2.Opcode
+import com.android.tools.smali.dexlib2.iface.instruction.formats.Instruction35c
+import com.android.tools.smali.dexlib2.iface.reference.MethodReference
+import com.android.tools.smali.dexlib2.immutable.reference.ImmutableMethodReference
+import com.android.tools.smali.dexlib2.util.MethodUtil
+
+private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/playintegrity/DisablePlayIntegrityPatch;"
+
+private val CONTEXT_BIND_SERVICE_METHOD_REFERENCE = ImmutableMethodReference(
+ "Landroid/content/Context;",
+ "bindService",
+ listOf("Landroid/content/Intent;", "Landroid/content/ServiceConnection;", "I"),
+ "Z"
+)
+
+
+@Suppress("unused")
+val disablePlayIntegrityPatch = bytecodePatch(
+ name = "Disable Play Integrity",
+ description = "Prevents apps from using Play Integrity by pretending it is not available.",
+ use = false,
+) {
+ extendWith("extensions/all/misc/disable-play-integrity.rve")
+
+ dependsOn(
+ transformInstructionsPatch(
+ filterMap = filterMap@{ classDef, method, instruction, instructionIndex ->
+ val reference = instruction
+ .getReference()
+ ?.takeIf {
+ MethodUtil.methodSignaturesMatch(CONTEXT_BIND_SERVICE_METHOD_REFERENCE, it)
+ }
+ ?: return@filterMap null
+
+ Triple(instruction as Instruction35c, instructionIndex, reference.parameterTypes)
+ },
+ transform = { method, entry ->
+ val (instruction, index, parameterTypes) = entry
+ val parameterString = parameterTypes.joinToString(separator = "")
+ val registerString = "v${instruction.registerC}, v${instruction.registerD}, v${instruction.registerE}, v${instruction.registerF}"
+
+ method.replaceInstruction(
+ index,
+ "invoke-static { $registerString }, $EXTENSION_CLASS_DESCRIPTOR->bindService(Landroid/content/Context;$parameterString)Z"
+ )
+ }
+ )
+ )
+}
From 4cc315952db557c565872de9e8484805f2e42305 Mon Sep 17 00:00:00 2001
From: xehpuk
Date: Thu, 8 Jan 2026 01:08:52 +0100
Subject: [PATCH 24/42] feat: Add `Disable Sentry telemetry` patch (#6416)
---
patches/api/patches.api | 4 +++
.../misc/privacy/DisableSentryTelemetry.kt | 36 +++++++++++++++++++
2 files changed, 40 insertions(+)
create mode 100644 patches/src/main/kotlin/app/revanced/patches/shared/misc/privacy/DisableSentryTelemetry.kt
diff --git a/patches/api/patches.api b/patches/api/patches.api
index ec75df2b3..86d8f980b 100644
--- a/patches/api/patches.api
+++ b/patches/api/patches.api
@@ -944,6 +944,10 @@ public final class app/revanced/patches/shared/misc/pairip/license/DisableLicens
public static final fun getDisableLicenseCheckPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
+public final class app/revanced/patches/shared/misc/privacy/DisableSentryTelemetryKt {
+ public static final fun getDisableSentryTelemetryPatch ()Lapp/revanced/patcher/patch/ResourcePatch;
+}
+
public final class app/revanced/patches/shared/misc/settings/SettingsPatchKt {
public static final fun overrideThemeColors (Ljava/lang/String;Ljava/lang/String;)V
public static final fun settingsPatch (Ljava/util/List;Ljava/util/Set;)Lapp/revanced/patcher/patch/ResourcePatch;
diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/misc/privacy/DisableSentryTelemetry.kt b/patches/src/main/kotlin/app/revanced/patches/shared/misc/privacy/DisableSentryTelemetry.kt
new file mode 100644
index 000000000..f361a26bb
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/shared/misc/privacy/DisableSentryTelemetry.kt
@@ -0,0 +1,36 @@
+package app.revanced.patches.shared.misc.privacy
+
+import app.revanced.patcher.patch.resourcePatch
+import app.revanced.util.asSequence
+import app.revanced.util.getNode
+import org.w3c.dom.Element
+
+@Suppress("unused")
+val disableSentryTelemetryPatch = resourcePatch(
+ name = "Disable Sentry telemetry",
+ description = "Disables Sentry telemetry. See https://sentry.io/for/android/ for more information.",
+ use = false,
+) {
+ execute {
+ fun Element.replaceOrCreate(tagName: String, attributeName: String, attributeValue: String) {
+ val childElements = getElementsByTagName(tagName).asSequence().filterIsInstance()
+ val targetChild = childElements.find { childElement ->
+ childElement.getAttribute("android:name") == attributeName
+ }
+ if (targetChild != null) {
+ targetChild.setAttribute("android:value", attributeValue)
+ } else {
+ appendChild(ownerDocument.createElement(tagName).apply {
+ setAttribute("android:name", attributeName)
+ setAttribute("android:value", attributeValue)
+ })
+ }
+ }
+
+ document("AndroidManifest.xml").use { document ->
+ val application = document.getNode("application") as Element
+ application.replaceOrCreate("meta-data", "io.sentry.enabled", "false")
+ application.replaceOrCreate("meta-data", "io.sentry.dsn", "")
+ }
+ }
+}
From f4af27dfecf24af25d1c1650bfd573485040a79c Mon Sep 17 00:00:00 2001
From: semantic-release-bot
Date: Thu, 8 Jan 2026 00:11:57 +0000
Subject: [PATCH 25/42] chore: Release v5.48.0-dev.9 [skip ci]
# [5.48.0-dev.9](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.8...v5.48.0-dev.9) (2026-01-08)
### Features
* Add `Disable Sentry telemetry` patch ([#6416](https://github.com/ReVanced/revanced-patches/issues/6416)) ([4cc3159](https://github.com/ReVanced/revanced-patches/commit/4cc315952db557c565872de9e8484805f2e42305))
* Disable Play Integrity patch ([#6412](https://github.com/ReVanced/revanced-patches/issues/6412)) ([6312fe8](https://github.com/ReVanced/revanced-patches/commit/6312fe8d60da24465c0c1b0fa4e94ceb79873d9c))
---
CHANGELOG.md | 8 ++++++++
gradle.properties | 2 +-
2 files changed, 9 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d9f397d0c..b70fb7efc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,11 @@
+# [5.48.0-dev.9](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.8...v5.48.0-dev.9) (2026-01-08)
+
+
+### Features
+
+* Add `Disable Sentry telemetry` patch ([#6416](https://github.com/ReVanced/revanced-patches/issues/6416)) ([4cc3159](https://github.com/ReVanced/revanced-patches/commit/4cc315952db557c565872de9e8484805f2e42305))
+* Disable Play Integrity patch ([#6412](https://github.com/ReVanced/revanced-patches/issues/6412)) ([6312fe8](https://github.com/ReVanced/revanced-patches/commit/6312fe8d60da24465c0c1b0fa4e94ceb79873d9c))
+
# [5.48.0-dev.8](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.7...v5.48.0-dev.8) (2026-01-04)
diff --git a/gradle.properties b/gradle.properties
index 5487ca1f0..816fb3e47 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -3,4 +3,4 @@ org.gradle.jvmargs = -Xms512M -Xmx2048M
org.gradle.parallel = true
android.useAndroidX = true
kotlin.code.style = official
-version = 5.48.0-dev.8
+version = 5.48.0-dev.9
From 004b5908db7913800e67305678116721103c17c1 Mon Sep 17 00:00:00 2001
From: Pun Butrach
Date: Sun, 11 Jan 2026 22:59:48 +0700
Subject: [PATCH 26/42] build: Use Gradle credentials system (#6467)
---
.github/workflows/build_pull_request.yml | 3 ++-
.github/workflows/release.yml | 3 ++-
patches/build.gradle.kts | 7 ++-----
settings.gradle.kts | 7 ++-----
4 files changed, 8 insertions(+), 12 deletions(-)
diff --git a/.github/workflows/build_pull_request.yml b/.github/workflows/build_pull_request.yml
index bb66d8526..8d0fdcd93 100644
--- a/.github/workflows/build_pull_request.yml
+++ b/.github/workflows/build_pull_request.yml
@@ -25,7 +25,8 @@ jobs:
- name: Build
env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ ORG_GRADLE_PROJECT_githubPackagesUsername: ${{ env.GITHUB_ACTOR }}
+ ORG_GRADLE_PROJECT_githubPackagesPassword: ${{ secrets.GITHUB_TOKEN }}
run: ./gradlew :patches:buildAndroid --no-daemon
- name: Upload artifacts
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 946dc9380..70790b6f6 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -31,7 +31,8 @@ jobs:
- name: Build
env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ ORG_GRADLE_PROJECT_githubPackagesUsername: ${{ env.GITHUB_ACTOR }}
+ ORG_GRADLE_PROJECT_githubPackagesPassword: ${{ secrets.GITHUB_TOKEN }}
run: ./gradlew :patches:buildAndroid clean
- name: Setup Node.js
diff --git a/patches/build.gradle.kts b/patches/build.gradle.kts
index 6153055b9..fa7bd65bd 100644
--- a/patches/build.gradle.kts
+++ b/patches/build.gradle.kts
@@ -50,12 +50,9 @@ kotlin {
publishing {
repositories {
maven {
- name = "GitHubPackages"
+ name = "githubPackages"
url = uri("https://maven.pkg.github.com/revanced/revanced-patches")
- credentials {
- username = System.getenv("GITHUB_ACTOR")
- password = System.getenv("GITHUB_TOKEN")
- }
+ credentials(PasswordCredentials::class)
}
}
}
\ No newline at end of file
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 357fde7cd..4ab39e0f4 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -5,12 +5,9 @@ pluginManagement {
gradlePluginPortal()
google()
maven {
- name = "GitHubPackages"
+ name = "githubPackages"
url = uri("https://maven.pkg.github.com/revanced/registry")
- credentials {
- username = providers.gradleProperty("gpr.user").getOrElse(System.getenv("GITHUB_ACTOR"))
- password = providers.gradleProperty("gpr.key").getOrElse(System.getenv("GITHUB_TOKEN"))
- }
+ credentials(PasswordCredentials::class)
}
}
}
From 12b819d20e7b39713f4c999d68e544397099a04b Mon Sep 17 00:00:00 2001
From: Pun Butrach
Date: Mon, 12 Jan 2026 08:40:19 +0700
Subject: [PATCH 27/42] ci: Schedule Crowdin to runs weekly instead of every 12
hours (#6466)
---
.github/workflows/pull_strings.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/pull_strings.yml b/.github/workflows/pull_strings.yml
index be27b0687..2ef910457 100644
--- a/.github/workflows/pull_strings.yml
+++ b/.github/workflows/pull_strings.yml
@@ -2,7 +2,7 @@ name: Pull strings
on:
schedule:
- - cron: "0 */12 * * *"
+ - cron: "0 0 * * 0"
workflow_dispatch:
jobs:
From 19f146c01dc381b3cccd61e61ba4901872ff12d8 Mon Sep 17 00:00:00 2001
From: ekaunt <62402760+ekaunt@users.noreply.github.com>
Date: Mon, 12 Jan 2026 17:18:09 -0500
Subject: [PATCH 28/42] feat(YouTube): Add `Pause on audio interrupt` patch
(#6464)
Co-authored-by: bengross
Co-authored-by: oSumAtrIX
---
.../patches/PauseOnAudioInterruptPatch.java | 30 +++++++++
.../extension/youtube/settings/Settings.java | 1 +
patches/api/patches.api | 4 ++
.../youtube/misc/audiofocus/Fingerprints.kt | 14 ++++
.../audiofocus/PauseOnAudioInterruptPatch.kt | 66 +++++++++++++++++++
.../resources/addresources/values/strings.xml | 5 ++
6 files changed, 120 insertions(+)
create mode 100644 extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/PauseOnAudioInterruptPatch.java
create mode 100644 patches/src/main/kotlin/app/revanced/patches/youtube/misc/audiofocus/Fingerprints.kt
create mode 100644 patches/src/main/kotlin/app/revanced/patches/youtube/misc/audiofocus/PauseOnAudioInterruptPatch.kt
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/PauseOnAudioInterruptPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/PauseOnAudioInterruptPatch.java
new file mode 100644
index 000000000..cf010a151
--- /dev/null
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/PauseOnAudioInterruptPatch.java
@@ -0,0 +1,30 @@
+package app.revanced.extension.youtube.patches;
+
+import app.revanced.extension.youtube.settings.Settings;
+
+@SuppressWarnings("unused")
+public class PauseOnAudioInterruptPatch {
+
+ private static final int AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK = -3;
+ private static final int AUDIOFOCUS_LOSS_TRANSIENT = -2;
+
+ /**
+ * Injection point for AudioFocusRequest builder.
+ * Returns true if audio ducking should be disabled (willPauseWhenDucked = true).
+ */
+ public static boolean shouldPauseOnAudioInterrupt() {
+ return Settings.PAUSE_ON_AUDIO_INTERRUPT.get();
+ }
+
+ /**
+ * Injection point for onAudioFocusChange callback.
+ * Converts AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK to AUDIOFOCUS_LOSS_TRANSIENT
+ * when the setting is enabled, causing YouTube to pause instead of ducking.
+ */
+ public static int overrideAudioFocusChange(int focusChange) {
+ if (Settings.PAUSE_ON_AUDIO_INTERRUPT.get() && focusChange == AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) {
+ return AUDIOFOCUS_LOSS_TRANSIENT;
+ }
+ return focusChange;
+ }
+}
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/Settings.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/Settings.java
index 62fa7bcae..21f3184b5 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/Settings.java
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/Settings.java
@@ -356,6 +356,7 @@ public class Settings extends BaseSettings {
public static final IntegerSetting ANNOUNCEMENT_LAST_ID = new IntegerSetting("revanced_announcement_last_id", -1, false, false);
public static final BooleanSetting LOOP_VIDEO = new BooleanSetting("revanced_loop_video", FALSE);
public static final BooleanSetting LOOP_VIDEO_BUTTON = new BooleanSetting("revanced_loop_video_button", FALSE);
+ public static final BooleanSetting PAUSE_ON_AUDIO_INTERRUPT = new BooleanSetting("revanced_pause_on_audio_interrupt", FALSE, true);
public static final BooleanSetting BYPASS_URL_REDIRECTS = new BooleanSetting("revanced_bypass_url_redirects", TRUE);
public static final BooleanSetting DISABLE_HAPTIC_FEEDBACK_CHAPTERS = new BooleanSetting("revanced_disable_haptic_feedback_chapters", FALSE);
public static final BooleanSetting DISABLE_HAPTIC_FEEDBACK_PRECISE_SEEKING = new BooleanSetting("revanced_disable_haptic_feedback_precise_seeking", FALSE);
diff --git a/patches/api/patches.api b/patches/api/patches.api
index 86d8f980b..8142a54dc 100644
--- a/patches/api/patches.api
+++ b/patches/api/patches.api
@@ -1696,6 +1696,10 @@ public final class app/revanced/patches/youtube/misc/announcements/Announcements
public static final fun getAnnouncementsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
+public final class app/revanced/patches/youtube/misc/audiofocus/PauseOnAudioInterruptPatchKt {
+ public static final fun getPauseOnAudioInterruptPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
+}
+
public final class app/revanced/patches/youtube/misc/autorepeat/AutoRepeatPatchKt {
public static final fun getAutoRepeatPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/audiofocus/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/audiofocus/Fingerprints.kt
new file mode 100644
index 000000000..5fb44f20f
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/audiofocus/Fingerprints.kt
@@ -0,0 +1,14 @@
+package app.revanced.patches.youtube.misc.audiofocus
+
+import app.revanced.patcher.fingerprint
+
+internal val audioFocusChangeListenerFingerprint = fingerprint {
+ strings(
+ "AudioFocus DUCK",
+ "AudioFocus loss; Will lower volume",
+ )
+}
+
+internal val audioFocusRequestBuilderFingerprint = fingerprint {
+ strings("Can't build an AudioFocusRequestCompat instance without a listener")
+}
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/audiofocus/PauseOnAudioInterruptPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/audiofocus/PauseOnAudioInterruptPatch.kt
new file mode 100644
index 000000000..2f6317d6e
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/audiofocus/PauseOnAudioInterruptPatch.kt
@@ -0,0 +1,66 @@
+package app.revanced.patches.youtube.misc.audiofocus
+
+import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
+import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels
+import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
+import app.revanced.patcher.patch.bytecodePatch
+import app.revanced.patcher.util.smali.ExternalLabel
+import app.revanced.patches.all.misc.resources.addResources
+import app.revanced.patches.all.misc.resources.addResourcesPatch
+import app.revanced.patches.shared.misc.settings.preference.SwitchPreference
+import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch
+import app.revanced.patches.youtube.misc.settings.PreferenceScreen
+import app.revanced.patches.youtube.misc.settings.settingsPatch
+
+private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/youtube/patches/PauseOnAudioInterruptPatch;"
+
+val pauseOnAudioInterruptPatch = bytecodePatch(
+ name = "Pause on audio interrupt",
+ description = "Adds an option to pause playback instead of lowering volume when other audio plays.",
+) {
+ dependsOn(
+ sharedExtensionPatch,
+ settingsPatch,
+ addResourcesPatch,
+ )
+
+ compatibleWith(
+ "com.google.android.youtube"(
+ "20.14.43",
+ )
+ )
+
+ execute {
+ addResources("youtube", "misc.audiofocus.pauseOnAudioInterruptPatch")
+
+ PreferenceScreen.MISC.addPreferences(
+ SwitchPreference("revanced_pause_on_audio_interrupt"),
+ )
+
+ // Hook the builder method that creates AudioFocusRequest.
+ // At the start, set the willPauseWhenDucked field (b) to true if setting is enabled.
+ val builderMethod = audioFocusRequestBuilderFingerprint.method
+ val builderClass = builderMethod.definingClass
+
+ builderMethod.addInstructionsWithLabels(
+ 0,
+ """
+ invoke-static {}, $EXTENSION_CLASS_DESCRIPTOR->shouldPauseOnAudioInterrupt()Z
+ move-result v0
+ if-eqz v0, :skip_override
+ const/4 v0, 0x1
+ iput-boolean v0, p0, $builderClass->b:Z
+ """,
+ ExternalLabel("skip_override", builderMethod.getInstruction(0)),
+ )
+
+ // Also hook the audio focus change listener as a backup.
+ audioFocusChangeListenerFingerprint.method.addInstructions(
+ 0,
+ """
+ invoke-static { p1 }, $EXTENSION_CLASS_DESCRIPTOR->overrideAudioFocusChange(I)I
+ move-result p1
+ """
+ )
+ }
+}
diff --git a/patches/src/main/resources/addresources/values/strings.xml b/patches/src/main/resources/addresources/values/strings.xml
index 53e99d70d..9c2a55e0e 100644
--- a/patches/src/main/resources/addresources/values/strings.xml
+++ b/patches/src/main/resources/addresources/values/strings.xml
@@ -1558,6 +1558,11 @@ Tap here to learn more about DeArrow"
Loop video is on
Loop video is off
+
+ Pause on audio interrupt
+ Playback pauses when other audio plays (e.g. navigation)
+ Volume lowers when other audio plays
+
Spoof device dimensions
"Device dimensions spoofed
From 778d13ce8b28ca6df3a665530320e4a21a27ae44 Mon Sep 17 00:00:00 2001
From: xehpuk
Date: Mon, 12 Jan 2026 23:28:15 +0100
Subject: [PATCH 29/42] feat(Strava): Add `Add media download` patch (#6449)
Co-authored-by: oSumAtrIX
---
.../app/revanced/extension/shared/Utils.java | 4 +
extensions/strava/build.gradle.kts | 5 +
.../strava/src/main/AndroidManifest.xml | 1 +
.../strava/AddMediaDownloadPatch.java | 216 +++++++++++++
extensions/strava/stub/build.gradle.kts | 12 +
.../strava/stub/src/main/AndroidManifest.xml | 1 +
.../com/strava/core/data/MediaContent.java | 15 +
.../com/strava/core/data/MediaDimension.java | 44 +++
.../java/com/strava/core/data/MediaType.java | 16 +
.../strava/core/data/RemoteMediaContent.java | 17 ++
.../strava/core/data/RemoteMediaStatus.java | 11 +
.../java/com/strava/photos/data/Media.java | 286 ++++++++++++++++++
patches/api/patches.api | 8 +
.../strava/media/AddMediaDownloadPatch.kt | 116 +++++++
.../patches/strava/media/Fingerprints.kt | 15 +
.../patches/strava/misc/extension/Hooks.kt | 9 +
.../misc/extension/SharedExtensionPatch.kt | 5 +
17 files changed, 781 insertions(+)
create mode 100644 extensions/strava/build.gradle.kts
create mode 100644 extensions/strava/src/main/AndroidManifest.xml
create mode 100644 extensions/strava/src/main/java/app/revanced/extension/strava/AddMediaDownloadPatch.java
create mode 100644 extensions/strava/stub/build.gradle.kts
create mode 100644 extensions/strava/stub/src/main/AndroidManifest.xml
create mode 100644 extensions/strava/stub/src/main/java/com/strava/core/data/MediaContent.java
create mode 100644 extensions/strava/stub/src/main/java/com/strava/core/data/MediaDimension.java
create mode 100644 extensions/strava/stub/src/main/java/com/strava/core/data/MediaType.java
create mode 100644 extensions/strava/stub/src/main/java/com/strava/core/data/RemoteMediaContent.java
create mode 100644 extensions/strava/stub/src/main/java/com/strava/core/data/RemoteMediaStatus.java
create mode 100644 extensions/strava/stub/src/main/java/com/strava/photos/data/Media.java
create mode 100644 patches/src/main/kotlin/app/revanced/patches/strava/media/AddMediaDownloadPatch.kt
create mode 100644 patches/src/main/kotlin/app/revanced/patches/strava/media/Fingerprints.kt
create mode 100644 patches/src/main/kotlin/app/revanced/patches/strava/misc/extension/Hooks.kt
create mode 100644 patches/src/main/kotlin/app/revanced/patches/strava/misc/extension/SharedExtensionPatch.kt
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/Utils.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/Utils.java
index 5d6bb492d..ea37cfcde 100644
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/Utils.java
+++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/Utils.java
@@ -311,6 +311,10 @@ public class Utils {
return resourceId;
}
+ public static String getResourceString(int id) throws Resources.NotFoundException {
+ return getContext().getResources().getString(id);
+ }
+
public static int getResourceInteger(String resourceIdentifierName) throws Resources.NotFoundException {
return getContext().getResources().getInteger(getResourceIdentifierOrThrow(resourceIdentifierName, "integer"));
}
diff --git a/extensions/strava/build.gradle.kts b/extensions/strava/build.gradle.kts
new file mode 100644
index 000000000..f282f41ea
--- /dev/null
+++ b/extensions/strava/build.gradle.kts
@@ -0,0 +1,5 @@
+dependencies {
+ compileOnly(project(":extensions:shared:library"))
+ compileOnly(project(":extensions:strava:stub"))
+ compileOnly(libs.okhttp)
+}
diff --git a/extensions/strava/src/main/AndroidManifest.xml b/extensions/strava/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..9b65eb06c
--- /dev/null
+++ b/extensions/strava/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+
diff --git a/extensions/strava/src/main/java/app/revanced/extension/strava/AddMediaDownloadPatch.java b/extensions/strava/src/main/java/app/revanced/extension/strava/AddMediaDownloadPatch.java
new file mode 100644
index 000000000..4c57cc792
--- /dev/null
+++ b/extensions/strava/src/main/java/app/revanced/extension/strava/AddMediaDownloadPatch.java
@@ -0,0 +1,216 @@
+package app.revanced.extension.strava;
+
+import android.annotation.SuppressLint;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Environment;
+import android.provider.MediaStore;
+import android.webkit.MimeTypeMap;
+
+import com.strava.core.data.MediaType;
+import com.strava.photos.data.Media;
+
+import okhttp3.*;
+
+import java.io.BufferedReader;
+import java.io.InputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import app.revanced.extension.shared.Utils;
+
+@SuppressLint("NewApi")
+public final class AddMediaDownloadPatch {
+ public static final int ACTION_DOWNLOAD = -1;
+ public static final int ACTION_OPEN_LINK = -2;
+ public static final int ACTION_COPY_LINK = -3;
+
+ private static final OkHttpClient client = new OkHttpClient();
+
+ public static boolean handleAction(int actionId, Media media) {
+ String url = getUrl(media);
+ switch (actionId) {
+ case ACTION_DOWNLOAD:
+ String name = media.getId();
+ if (media.getType() == MediaType.VIDEO) {
+ downloadVideo(url, name);
+ } else {
+ downloadPhoto(url, name);
+ }
+ return true;
+ case ACTION_OPEN_LINK:
+ Utils.openLink(url);
+ return true;
+ case ACTION_COPY_LINK:
+ copyLink(url);
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ public static void copyLink(CharSequence url) {
+ Utils.setClipboard(url);
+ showInfoToast("link_copied_to_clipboard", "🔗");
+ }
+
+ public static void downloadPhoto(String url, String name) {
+ showInfoToast("loading", "⏳");
+ Utils.runOnBackgroundThread(() -> {
+ try (Response response = fetch(url)) {
+ ResponseBody body = response.body();
+ String mimeType = body.contentType().toString();
+ String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType);
+ ContentResolver resolver = Utils.getContext().getContentResolver();
+ ContentValues values = new ContentValues();
+ values.put(MediaStore.Images.Media.DISPLAY_NAME, name + '.' + extension);
+ values.put(MediaStore.Images.Media.IS_PENDING, 1);
+ values.put(MediaStore.Images.Media.MIME_TYPE, mimeType);
+ values.put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + "/Strava");
+ Uri collection = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
+ ? MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL)
+ : MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
+ Uri row = resolver.insert(collection, values);
+ try (OutputStream outputStream = resolver.openOutputStream(row)) {
+ transferTo(body.byteStream(), outputStream);
+ } finally {
+ values.clear();
+ values.put(MediaStore.Images.Media.IS_PENDING, 0);
+ resolver.update(row, values, null);
+ }
+ showInfoToast("yis_2024_local_save_image_success", "✔️");
+ } catch (IOException e) {
+ showErrorToast("download_failure", "❌", e);
+ }
+ });
+ }
+
+ /**
+ * Downloads a video in the M3U8 / HLS (HTTP Live Streaming) format.
+ */
+ public static void downloadVideo(String url, String name) {
+ // The first request yields multiple URLs with different stream options.
+ // In case of Strava, the first one is always of highest quality.
+ // Each stream can consist of multiple chunks.
+ // The second request yields the URLs of all of these chunks.
+ // Fetch all of them concurrently and pipe their streams into the file in order.
+ showInfoToast("loading", "⏳");
+ Utils.runOnBackgroundThread(() -> {
+ try {
+ String highestQualityStreamUrl;
+ try (Response response = fetch(url)) {
+ highestQualityStreamUrl = replaceFileName(url, lines(response).findFirst().get());
+ }
+ List> futures;
+ try (Response response = fetch(highestQualityStreamUrl)) {
+ futures = lines(response)
+ .map(line -> replaceFileName(highestQualityStreamUrl, line))
+ .map(chunkUrl -> Utils.submitOnBackgroundThread(() -> fetch(chunkUrl)))
+ .collect(Collectors.toList());
+ }
+ ContentResolver resolver = Utils.getContext().getContentResolver();
+ ContentValues values = new ContentValues();
+ values.put(MediaStore.Video.Media.DISPLAY_NAME, name + '.' + "mp4");
+ values.put(MediaStore.Video.Media.IS_PENDING, 1);
+ values.put(MediaStore.Video.Media.MIME_TYPE, MimeTypeMap.getSingleton().getMimeTypeFromExtension("mp4"));
+ values.put(MediaStore.Video.Media.RELATIVE_PATH, Environment.DIRECTORY_MOVIES + "/Strava");
+ Uri collection = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
+ ? MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL)
+ : MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
+ Uri row = resolver.insert(collection, values);
+ try (OutputStream outputStream = resolver.openOutputStream(row)) {
+ Throwable error = null;
+ for (Future future : futures) {
+ if (error != null) {
+ if (future.cancel(true)) {
+ continue;
+ }
+ }
+ try (Response response = future.get()) {
+ if (error == null) {
+ transferTo(response.body().byteStream(), outputStream);
+ }
+ } catch (InterruptedException | IOException e) {
+ error = e;
+ } catch (ExecutionException e) {
+ error = e.getCause();
+ }
+ }
+ if (error != null) {
+ throw new IOException(error);
+ }
+ } finally {
+ values.clear();
+ values.put(MediaStore.Video.Media.IS_PENDING, 0);
+ resolver.update(row, values, null);
+ }
+ showInfoToast("yis_2024_local_save_video_success", "✔️");
+ } catch (IOException e) {
+ showErrorToast("download_failure", "❌", e);
+ }
+ });
+ }
+
+ private static String getUrl(Media media) {
+ return media.getType() == MediaType.VIDEO
+ ? ((Media.Video) media).getVideoUrl()
+ : media.getLargestUrl();
+ }
+
+ private static String getString(String name, String fallback) {
+ int id = Utils.getResourceIdentifier(name, "string");
+ return id != 0
+ ? Utils.getResourceString(id)
+ : fallback;
+ }
+
+ private static void showInfoToast(String resourceName, String fallback) {
+ String text = getString(resourceName, fallback);
+ Utils.showToastShort(text);
+ }
+
+ private static void showErrorToast(String resourceName, String fallback, IOException exception) {
+ String text = getString(resourceName, fallback);
+ Utils.showToastLong(text + ' ' + exception.getLocalizedMessage());
+ }
+
+ private static Response fetch(String url) throws IOException {
+ Request request = new Request.Builder().url(url).build();
+ Response response = client.newCall(request).execute();
+ if (!response.isSuccessful()) {
+ throw new IOException("Got HTTP status code " + response.code());
+ }
+ return response;
+ }
+
+ /**
+ * {@code inputStream.transferTo(outputStream)} is "too new".
+ */
+ private static void transferTo(InputStream in, OutputStream out) throws IOException {
+ byte[] buffer = new byte[1024 * 8];
+ int length;
+ while ((length = in.read(buffer)) != -1) {
+ out.write(buffer, 0, length);
+ }
+ }
+
+ /**
+ * Gets all file names.
+ */
+ private static Stream lines(Response response) {
+ BufferedReader reader = new BufferedReader(response.body().charStream());
+ return reader.lines().filter(line -> !line.startsWith("#"));
+ }
+
+ private static String replaceFileName(String uri, String newName) {
+ return uri.substring(0, uri.lastIndexOf('/') + 1) + newName;
+ }
+}
diff --git a/extensions/strava/stub/build.gradle.kts b/extensions/strava/stub/build.gradle.kts
new file mode 100644
index 000000000..ffdfac5a6
--- /dev/null
+++ b/extensions/strava/stub/build.gradle.kts
@@ -0,0 +1,12 @@
+plugins {
+ alias(libs.plugins.android.library)
+}
+
+android {
+ namespace = "app.revanced.extension"
+ compileSdk = 34
+
+ defaultConfig {
+ minSdk = 21
+ }
+}
diff --git a/extensions/strava/stub/src/main/AndroidManifest.xml b/extensions/strava/stub/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..15e7c2ae6
--- /dev/null
+++ b/extensions/strava/stub/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/extensions/strava/stub/src/main/java/com/strava/core/data/MediaContent.java b/extensions/strava/stub/src/main/java/com/strava/core/data/MediaContent.java
new file mode 100644
index 000000000..ba3ab4ecd
--- /dev/null
+++ b/extensions/strava/stub/src/main/java/com/strava/core/data/MediaContent.java
@@ -0,0 +1,15 @@
+package com.strava.core.data;
+
+import java.io.Serializable;
+
+public interface MediaContent extends Serializable {
+ String getCaption();
+
+ String getId();
+
+ String getReferenceId();
+
+ MediaType getType();
+
+ void setCaption(String caption);
+}
diff --git a/extensions/strava/stub/src/main/java/com/strava/core/data/MediaDimension.java b/extensions/strava/stub/src/main/java/com/strava/core/data/MediaDimension.java
new file mode 100644
index 000000000..6f4b3d104
--- /dev/null
+++ b/extensions/strava/stub/src/main/java/com/strava/core/data/MediaDimension.java
@@ -0,0 +1,44 @@
+package com.strava.core.data;
+
+import java.io.Serializable;
+
+public final class MediaDimension implements Comparable, Serializable {
+ private final int height;
+ private final int width;
+
+ public MediaDimension(int width, int height) {
+ this.width = width;
+ this.height = height;
+ }
+
+ public int getHeight() {
+ return height;
+ }
+
+ public float getHeightScale() {
+ if (width <= 0 || height <= 0) {
+ return 1f;
+ }
+ return height / width;
+ }
+
+ public int getWidth() {
+ return width;
+ }
+
+ public float getWidthScale() {
+ if (width <= 0 || height <= 0) {
+ return 1f;
+ }
+ return width / height;
+ }
+
+ public boolean isLandscape() {
+ return width > 0 && width >= height;
+ }
+
+ @Override
+ public int compareTo(MediaDimension other) {
+ return 0;
+ }
+}
diff --git a/extensions/strava/stub/src/main/java/com/strava/core/data/MediaType.java b/extensions/strava/stub/src/main/java/com/strava/core/data/MediaType.java
new file mode 100644
index 000000000..7fb100b5c
--- /dev/null
+++ b/extensions/strava/stub/src/main/java/com/strava/core/data/MediaType.java
@@ -0,0 +1,16 @@
+package com.strava.core.data;
+
+public enum MediaType {
+ PHOTO(1),
+ VIDEO(2);
+
+ private final int remoteValue;
+
+ private MediaType(int remoteValue) {
+ this.remoteValue = remoteValue;
+ }
+
+ public int getRemoteValue() {
+ return remoteValue;
+ }
+}
diff --git a/extensions/strava/stub/src/main/java/com/strava/core/data/RemoteMediaContent.java b/extensions/strava/stub/src/main/java/com/strava/core/data/RemoteMediaContent.java
new file mode 100644
index 000000000..4c190ab56
--- /dev/null
+++ b/extensions/strava/stub/src/main/java/com/strava/core/data/RemoteMediaContent.java
@@ -0,0 +1,17 @@
+package com.strava.core.data;
+
+import java.util.SortedMap;
+
+public interface RemoteMediaContent extends MediaContent {
+ MediaDimension getLargestSize();
+
+ String getLargestUrl();
+
+ SortedMap getSizes();
+
+ String getSmallestUrl();
+
+ RemoteMediaStatus getStatus();
+
+ SortedMap getUrls();
+}
diff --git a/extensions/strava/stub/src/main/java/com/strava/core/data/RemoteMediaStatus.java b/extensions/strava/stub/src/main/java/com/strava/core/data/RemoteMediaStatus.java
new file mode 100644
index 000000000..65dda77ed
--- /dev/null
+++ b/extensions/strava/stub/src/main/java/com/strava/core/data/RemoteMediaStatus.java
@@ -0,0 +1,11 @@
+package com.strava.core.data;
+
+public enum RemoteMediaStatus {
+ NEW,
+ PENDING,
+ PROCESSED,
+ REPORTED,
+ REINSTATED,
+ DELETED,
+ FAILED
+}
diff --git a/extensions/strava/stub/src/main/java/com/strava/photos/data/Media.java b/extensions/strava/stub/src/main/java/com/strava/photos/data/Media.java
new file mode 100644
index 000000000..46b4add71
--- /dev/null
+++ b/extensions/strava/stub/src/main/java/com/strava/photos/data/Media.java
@@ -0,0 +1,286 @@
+package com.strava.photos.data;
+
+import com.strava.core.data.MediaDimension;
+import com.strava.core.data.MediaType;
+import com.strava.core.data.RemoteMediaContent;
+import com.strava.core.data.RemoteMediaStatus;
+import java.util.SortedMap;
+
+public abstract class Media implements RemoteMediaContent {
+ public static final class Photo extends Media {
+ private final Long activityId;
+ private final String activityName;
+ private final long athleteId;
+ private String caption;
+ private final String createdAt;
+ private final String createdAtLocal;
+ private final String cursor;
+ private final String id;
+ private final SortedMap sizes;
+ private final RemoteMediaStatus status;
+ private final String tag;
+ private final MediaType type;
+ private final SortedMap urls;
+
+ @Override
+ public Long getActivityId() {
+ return activityId;
+ }
+
+ @Override
+ public String getActivityName() {
+ return activityName;
+ }
+
+ @Override
+ public long getAthleteId() {
+ return athleteId;
+ }
+
+ @Override
+ public String getCaption() {
+ return caption;
+ }
+
+ @Override
+ public String getCreatedAt() {
+ return createdAt;
+ }
+
+ @Override
+ public String getCreatedAtLocal() {
+ return createdAtLocal;
+ }
+
+ @Override
+ public String getCursor() {
+ return cursor;
+ }
+
+ @Override
+ public String getId() {
+ return id;
+ }
+
+ @Override
+ public SortedMap getSizes() {
+ return sizes;
+ }
+
+ @Override
+ public RemoteMediaStatus getStatus() {
+ return status;
+ }
+
+ @Override
+ public String getTag() {
+ return tag;
+ }
+
+ @Override
+ public MediaType getType() {
+ return type;
+ }
+
+ @Override
+ public SortedMap getUrls() {
+ return urls;
+ }
+
+ @Override
+ public void setCaption(String caption) {
+ this.caption = caption;
+ }
+
+ public Photo(String id,
+ String caption,
+ SortedMap urls,
+ SortedMap sizes,
+ long athleteId,
+ String createdAt,
+ String createdAtLocal,
+ Long activityId,
+ String activityName,
+ RemoteMediaStatus status,
+ String tag,
+ String cursor) {
+ this.id = id;
+ this.caption = caption;
+ this.urls = urls;
+ this.sizes = sizes;
+ this.athleteId = athleteId;
+ this.createdAt = createdAt;
+ this.createdAtLocal = createdAtLocal;
+ this.activityId = activityId;
+ this.activityName = activityName;
+ this.status = status;
+ this.tag = tag;
+ this.cursor = cursor;
+ this.type = MediaType.PHOTO;
+ }
+ }
+
+ public static final class Video extends Media {
+ private final Long activityId;
+ private final String activityName;
+ private final long athleteId;
+ private String caption;
+ private final String createdAt;
+ private final String createdAtLocal;
+ private final String cursor;
+ private final Float durationSeconds;
+ private final String id;
+ private final SortedMap sizes;
+ private final RemoteMediaStatus status;
+ private final String tag;
+ private final MediaType type;
+ private final SortedMap urls;
+ private final String videoUrl;
+
+ @Override
+ public Long getActivityId() {
+ return activityId;
+ }
+
+ @Override
+ public String getActivityName() {
+ return activityName;
+ }
+
+ @Override
+ public long getAthleteId() {
+ return athleteId;
+ }
+
+ @Override
+ public String getCaption() {
+ return caption;
+ }
+
+ @Override
+ public String getCreatedAt() {
+ return createdAt;
+ }
+
+ @Override
+ public String getCreatedAtLocal() {
+ return createdAtLocal;
+ }
+
+ @Override
+ public String getCursor() {
+ return cursor;
+ }
+
+ public final Float getDurationSeconds() {
+ return durationSeconds;
+ }
+
+ @Override
+ public String getId() {
+ return id;
+ }
+
+ @Override
+ public SortedMap getSizes() {
+ return sizes;
+ }
+
+ @Override
+ public RemoteMediaStatus getStatus() {
+ return status;
+ }
+
+ @Override
+ public String getTag() {
+ return tag;
+ }
+
+ @Override
+ public MediaType getType() {
+ return type;
+ }
+
+ @Override
+ public SortedMap getUrls() {
+ return urls;
+ }
+
+ public final String getVideoUrl() {
+ return videoUrl;
+ }
+
+ @Override
+ public void setCaption(String caption) {
+ this.caption = caption;
+ }
+
+ public Video(String id,
+ String caption,
+ SortedMap urls,
+ SortedMap sizes,
+ long athleteId,
+ String createdAt,
+ String createdAtLocal,
+ Long activityId,
+ String activityName,
+ RemoteMediaStatus status,
+ String videoUrl,
+ Float durationSeconds,
+ String tag,
+ String cursor) {
+ this.id = id;
+ this.caption = caption;
+ this.urls = urls;
+ this.sizes = sizes;
+ this.athleteId = athleteId;
+ this.createdAt = createdAt;
+ this.createdAtLocal = createdAtLocal;
+ this.activityId = activityId;
+ this.activityName = activityName;
+ this.status = status;
+ this.videoUrl = videoUrl;
+ this.durationSeconds = durationSeconds;
+ this.tag = tag;
+ this.cursor = cursor;
+ this.type = MediaType.VIDEO;
+ }
+ }
+
+ public abstract Long getActivityId();
+
+ public abstract String getActivityName();
+
+ public abstract long getAthleteId();
+
+ public abstract String getCreatedAt();
+
+ public abstract String getCreatedAtLocal();
+
+ public abstract String getCursor();
+
+ @Override
+ public MediaDimension getLargestSize() {
+ return null;
+ }
+
+ @Override
+ public String getLargestUrl() {
+ return null;
+ }
+
+ @Override
+ public String getReferenceId() {
+ return null;
+ }
+
+ @Override
+ public String getSmallestUrl() {
+ return null;
+ }
+
+ public abstract String getTag();
+
+ private Media() {
+ }
+}
diff --git a/patches/api/patches.api b/patches/api/patches.api
index 8142a54dc..91942f440 100644
--- a/patches/api/patches.api
+++ b/patches/api/patches.api
@@ -1200,10 +1200,18 @@ public final class app/revanced/patches/stocard/layout/HideStoryBubblesPatchKt {
public static final fun getHideStoryBubblesPatch ()Lapp/revanced/patcher/patch/ResourcePatch;
}
+public final class app/revanced/patches/strava/media/AddMediaDownloadPatchKt {
+ public static final fun getAddMediaDownloadPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
+}
+
public final class app/revanced/patches/strava/mediaupload/OverwriteMediaUploadParametersPatchKt {
public static final fun getOverwriteMediaUploadParametersPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
+public final class app/revanced/patches/strava/misc/extension/SharedExtensionPatchKt {
+ public static final fun getSharedExtensionPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
+}
+
public final class app/revanced/patches/strava/password/EnablePasswordLoginPatchKt {
public static final fun getEnablePasswordLoginPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
diff --git a/patches/src/main/kotlin/app/revanced/patches/strava/media/AddMediaDownloadPatch.kt b/patches/src/main/kotlin/app/revanced/patches/strava/media/AddMediaDownloadPatch.kt
new file mode 100644
index 000000000..227b3e675
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/strava/media/AddMediaDownloadPatch.kt
@@ -0,0 +1,116 @@
+package app.revanced.patches.strava.media
+
+import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
+import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
+import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels
+import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
+import app.revanced.patcher.extensions.InstructionExtensions.instructions
+import app.revanced.patcher.patch.bytecodePatch
+import app.revanced.patcher.util.smali.ExternalLabel
+import app.revanced.patches.shared.misc.mapping.get
+import app.revanced.patches.shared.misc.mapping.resourceMappingPatch
+import app.revanced.patches.shared.misc.mapping.resourceMappings
+import app.revanced.patches.strava.misc.extension.sharedExtensionPatch
+import app.revanced.util.getReference
+import app.revanced.util.writeRegister
+import com.android.tools.smali.dexlib2.Opcode
+import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction22c
+import com.android.tools.smali.dexlib2.iface.reference.FieldReference
+import com.android.tools.smali.dexlib2.iface.reference.TypeReference
+
+private const val ACTION_CLASS_DESCRIPTOR = "Lcom/strava/bottomsheet/Action;"
+private const val MEDIA_CLASS_DESCRIPTOR = "Lcom/strava/photos/data/Media;"
+private const val MEDIA_DOWNLOAD_CLASS_DESCRIPTOR = "Lapp/revanced/extension/strava/AddMediaDownloadPatch;"
+
+@Suppress("unused")
+val addMediaDownloadPatch = bytecodePatch(
+ name = "Add media download",
+ description = "Extends the full-screen media viewer menu with items to copy or open their URLs or download them directly."
+) {
+ compatibleWith("com.strava")
+
+ dependsOn(
+ resourceMappingPatch,
+ sharedExtensionPatch
+ )
+
+ execute {
+ val fragmentClass = classBy { it.endsWith("/FullscreenMediaFragment;") }!!.mutableClass
+
+ // region Extend menu of `FullscreenMediaFragment` with actions.
+
+ createAndShowFragmentFingerprint.match(fragmentClass).method.apply {
+ val setTrueIndex = instructions.indexOfFirst { instruction ->
+ instruction.opcode == Opcode.IPUT_BOOLEAN
+ }
+ val actionRegistrarRegister = getInstruction(setTrueIndex).registerB
+ val actionRegister = instructions.first { instruction ->
+ instruction.getReference()?.type == ACTION_CLASS_DESCRIPTOR
+ }.writeRegister!!
+
+ fun addMenuItem(actionId: String, string: String, color: String, drawable: String) = addInstructions(
+ setTrueIndex + 1,
+ """
+ new-instance v$actionRegister, $ACTION_CLASS_DESCRIPTOR
+ sget v${actionRegister + 1}, $MEDIA_DOWNLOAD_CLASS_DESCRIPTOR->$actionId:I
+ const v${actionRegister + 2}, 0x0
+ const v${actionRegister + 3}, ${resourceMappings["string", string]}
+ const v${actionRegister + 4}, ${resourceMappings["color", color]}
+ const v${actionRegister + 5}, ${resourceMappings["drawable", drawable]}
+ move/from16 v${actionRegister + 6}, v${actionRegister + 4}
+ invoke-direct/range { v$actionRegister .. v${actionRegister + 7} }, $ACTION_CLASS_DESCRIPTOR->(ILjava/lang/String;IIIILjava/io/Serializable;)V
+ invoke-virtual { v$actionRegistrarRegister, v$actionRegister }, Lcom/strava/bottomsheet/a;->a(Lcom/strava/bottomsheet/BottomSheetItem;)V
+ """
+ )
+
+ addMenuItem("ACTION_COPY_LINK", "copy_link", "core_o3", "actions_link_normal_xsmall")
+ addMenuItem("ACTION_OPEN_LINK", "fallback_menu_item_open_in_browser", "core_o3", "actions_link_external_normal_xsmall")
+ addMenuItem("ACTION_DOWNLOAD", "download", "core_o3", "actions_download_normal_xsmall")
+
+ // Move media to last parameter of `Action` constructor.
+ val getMediaInstruction = instructions.first { instruction ->
+ instruction.getReference()?.type == MEDIA_CLASS_DESCRIPTOR
+ }
+ addInstruction(
+ getMediaInstruction.location.index + 1,
+ "move-object/from16 v${actionRegister + 7}, v${getMediaInstruction.writeRegister}"
+ )
+ }
+
+ // endregion
+
+ // region Handle new actions.
+
+ val actionClass = classes.first { clazz ->
+ clazz.type == ACTION_CLASS_DESCRIPTOR
+ }
+ val actionSerializableField = actionClass.instanceFields.first { field ->
+ field.type == "Ljava/io/Serializable;"
+ }
+
+ // Handle "copy link" & "open link" & "download" actions.
+ handleMediaActionFingerprint.match(fragmentClass).method.apply {
+ // Call handler if action ID < 0 (= custom).
+ val moveInstruction = instructions.first { instruction ->
+ instruction.opcode == Opcode.MOVE_RESULT
+ }
+ val indexAfterMoveInstruction = moveInstruction.location.index + 1
+ val actionIdRegister = moveInstruction.writeRegister
+ addInstructionsWithLabels(
+ indexAfterMoveInstruction,
+ """
+ if-gez v$actionIdRegister, :move
+ check-cast p2, $ACTION_CLASS_DESCRIPTOR
+ iget-object v0, p2, $actionSerializableField
+ check-cast v0, $MEDIA_CLASS_DESCRIPTOR
+ invoke-static { v$actionIdRegister, v0 }, $MEDIA_DOWNLOAD_CLASS_DESCRIPTOR->handleAction(I$MEDIA_CLASS_DESCRIPTOR)Z
+ move-result v0
+ return v0
+ """,
+ ExternalLabel("move", instructions[indexAfterMoveInstruction])
+ )
+ }
+
+ // endregion
+ }
+}
diff --git a/patches/src/main/kotlin/app/revanced/patches/strava/media/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/strava/media/Fingerprints.kt
new file mode 100644
index 000000000..ebdf10fda
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/strava/media/Fingerprints.kt
@@ -0,0 +1,15 @@
+package app.revanced.patches.strava.media
+
+import app.revanced.patcher.fingerprint
+import com.android.tools.smali.dexlib2.AccessFlags
+
+internal val createAndShowFragmentFingerprint = fingerprint {
+ accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
+ returns("V")
+ parameters("L")
+ strings("mediaType")
+}
+
+internal val handleMediaActionFingerprint = fingerprint {
+ parameters("Landroid/view/View;", "Lcom/strava/bottomsheet/BottomSheetItem;")
+}
diff --git a/patches/src/main/kotlin/app/revanced/patches/strava/misc/extension/Hooks.kt b/patches/src/main/kotlin/app/revanced/patches/strava/misc/extension/Hooks.kt
new file mode 100644
index 000000000..6c2a97769
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/strava/misc/extension/Hooks.kt
@@ -0,0 +1,9 @@
+package app.revanced.patches.strava.misc.extension
+
+import app.revanced.patches.shared.misc.extension.extensionHook
+
+internal val applicationOnCreateHook = extensionHook {
+ custom { method, classDef ->
+ method.name == "onCreate" && classDef.endsWith("/StravaApplication;")
+ }
+}
diff --git a/patches/src/main/kotlin/app/revanced/patches/strava/misc/extension/SharedExtensionPatch.kt b/patches/src/main/kotlin/app/revanced/patches/strava/misc/extension/SharedExtensionPatch.kt
new file mode 100644
index 000000000..404bf6e9a
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/strava/misc/extension/SharedExtensionPatch.kt
@@ -0,0 +1,5 @@
+package app.revanced.patches.strava.misc.extension
+
+import app.revanced.patches.shared.misc.extension.sharedExtensionPatch
+
+val sharedExtensionPatch = sharedExtensionPatch("strava", applicationOnCreateHook)
From 41e2590584be1de0f3f7c508584d2ab836a0ec12 Mon Sep 17 00:00:00 2001
From: xehpuk
Date: Tue, 13 Jan 2026 15:46:49 +0100
Subject: [PATCH 30/42] chore(Strava): Restructure `media` package (#6480)
---
patches/api/patches.api | 4 ++--
.../strava/media/{ => download}/AddMediaDownloadPatch.kt | 2 +-
.../patches/strava/media/{ => download}/Fingerprints.kt | 2 +-
.../strava/{mediaupload => media/upload}/Fingerprints.kt | 2 +-
.../upload}/OverwriteMediaUploadParametersPatch.kt | 2 +-
5 files changed, 6 insertions(+), 6 deletions(-)
rename patches/src/main/kotlin/app/revanced/patches/strava/media/{ => download}/AddMediaDownloadPatch.kt (99%)
rename patches/src/main/kotlin/app/revanced/patches/strava/media/{ => download}/Fingerprints.kt (89%)
rename patches/src/main/kotlin/app/revanced/patches/strava/{mediaupload => media/upload}/Fingerprints.kt (89%)
rename patches/src/main/kotlin/app/revanced/patches/strava/{mediaupload => media/upload}/OverwriteMediaUploadParametersPatch.kt (97%)
diff --git a/patches/api/patches.api b/patches/api/patches.api
index 91942f440..c14ba4a53 100644
--- a/patches/api/patches.api
+++ b/patches/api/patches.api
@@ -1200,11 +1200,11 @@ public final class app/revanced/patches/stocard/layout/HideStoryBubblesPatchKt {
public static final fun getHideStoryBubblesPatch ()Lapp/revanced/patcher/patch/ResourcePatch;
}
-public final class app/revanced/patches/strava/media/AddMediaDownloadPatchKt {
+public final class app/revanced/patches/strava/media/download/AddMediaDownloadPatchKt {
public static final fun getAddMediaDownloadPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
-public final class app/revanced/patches/strava/mediaupload/OverwriteMediaUploadParametersPatchKt {
+public final class app/revanced/patches/strava/media/upload/OverwriteMediaUploadParametersPatchKt {
public static final fun getOverwriteMediaUploadParametersPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
diff --git a/patches/src/main/kotlin/app/revanced/patches/strava/media/AddMediaDownloadPatch.kt b/patches/src/main/kotlin/app/revanced/patches/strava/media/download/AddMediaDownloadPatch.kt
similarity index 99%
rename from patches/src/main/kotlin/app/revanced/patches/strava/media/AddMediaDownloadPatch.kt
rename to patches/src/main/kotlin/app/revanced/patches/strava/media/download/AddMediaDownloadPatch.kt
index 227b3e675..01da305c9 100644
--- a/patches/src/main/kotlin/app/revanced/patches/strava/media/AddMediaDownloadPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/strava/media/download/AddMediaDownloadPatch.kt
@@ -1,4 +1,4 @@
-package app.revanced.patches.strava.media
+package app.revanced.patches.strava.media.download
import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
diff --git a/patches/src/main/kotlin/app/revanced/patches/strava/media/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/strava/media/download/Fingerprints.kt
similarity index 89%
rename from patches/src/main/kotlin/app/revanced/patches/strava/media/Fingerprints.kt
rename to patches/src/main/kotlin/app/revanced/patches/strava/media/download/Fingerprints.kt
index ebdf10fda..2f6566a82 100644
--- a/patches/src/main/kotlin/app/revanced/patches/strava/media/Fingerprints.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/strava/media/download/Fingerprints.kt
@@ -1,4 +1,4 @@
-package app.revanced.patches.strava.media
+package app.revanced.patches.strava.media.download
import app.revanced.patcher.fingerprint
import com.android.tools.smali.dexlib2.AccessFlags
diff --git a/patches/src/main/kotlin/app/revanced/patches/strava/mediaupload/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/strava/media/upload/Fingerprints.kt
similarity index 89%
rename from patches/src/main/kotlin/app/revanced/patches/strava/mediaupload/Fingerprints.kt
rename to patches/src/main/kotlin/app/revanced/patches/strava/media/upload/Fingerprints.kt
index 7901d6cc5..9653ff76b 100644
--- a/patches/src/main/kotlin/app/revanced/patches/strava/mediaupload/Fingerprints.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/strava/media/upload/Fingerprints.kt
@@ -1,4 +1,4 @@
-package app.revanced.patches.strava.mediaupload
+package app.revanced.patches.strava.media.upload
import app.revanced.patcher.fingerprint
diff --git a/patches/src/main/kotlin/app/revanced/patches/strava/mediaupload/OverwriteMediaUploadParametersPatch.kt b/patches/src/main/kotlin/app/revanced/patches/strava/media/upload/OverwriteMediaUploadParametersPatch.kt
similarity index 97%
rename from patches/src/main/kotlin/app/revanced/patches/strava/mediaupload/OverwriteMediaUploadParametersPatch.kt
rename to patches/src/main/kotlin/app/revanced/patches/strava/media/upload/OverwriteMediaUploadParametersPatch.kt
index 182306c6d..1ae9bc18c 100644
--- a/patches/src/main/kotlin/app/revanced/patches/strava/mediaupload/OverwriteMediaUploadParametersPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/strava/media/upload/OverwriteMediaUploadParametersPatch.kt
@@ -1,4 +1,4 @@
-package app.revanced.patches.strava.mediaupload
+package app.revanced.patches.strava.media.upload
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patcher.patch.intOption
From 87247590de3db74680cb02ba1d87bf683b2269e2 Mon Sep 17 00:00:00 2001
From: Swakshan <56347042+Swakshan@users.noreply.github.com>
Date: Wed, 14 Jan 2026 00:40:25 +0530
Subject: [PATCH 31/42] fix(Instagram): `Sanitize sharing links` (#6483)
---
.../misc/share/PermalinkResponseJsonParserFingerprint.kt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/patches/src/main/kotlin/app/revanced/patches/instagram/misc/share/PermalinkResponseJsonParserFingerprint.kt b/patches/src/main/kotlin/app/revanced/patches/instagram/misc/share/PermalinkResponseJsonParserFingerprint.kt
index 9e0d8e64d..9f2d9fc4e 100644
--- a/patches/src/main/kotlin/app/revanced/patches/instagram/misc/share/PermalinkResponseJsonParserFingerprint.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/instagram/misc/share/PermalinkResponseJsonParserFingerprint.kt
@@ -13,7 +13,7 @@ internal val storyUrlResponseJsonParserFingerprint = fingerprint {
}
internal val profileUrlResponseJsonParserFingerprint = fingerprint {
- strings("profile_to_share_url", "ProfileUrlResponse")
+ strings("profile_to_share_url")
custom { method, _ -> method.name == "parseFromJson" }
}
From 3401467a6d49fc75b6757a15e5c848330c1b7307 Mon Sep 17 00:00:00 2001
From: oSumAtrIX
Date: Thu, 15 Jan 2026 23:30:17 +0100
Subject: [PATCH 32/42] feat(Instagram): Disable `Disable Reels scrolling` by
default
---
.../patches/instagram/reels/DisableReelsScrollingPatch.kt | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/patches/src/main/kotlin/app/revanced/patches/instagram/reels/DisableReelsScrollingPatch.kt b/patches/src/main/kotlin/app/revanced/patches/instagram/reels/DisableReelsScrollingPatch.kt
index acb8235fb..059a30065 100644
--- a/patches/src/main/kotlin/app/revanced/patches/instagram/reels/DisableReelsScrollingPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/instagram/reels/DisableReelsScrollingPatch.kt
@@ -9,7 +9,7 @@ val disableReelsScrollingPatch = bytecodePatch(
name = "Disable Reels scrolling",
description = "Disables the endless scrolling behavior in Instagram Reels, preventing swiping to the next Reel. " +
"Note: On a clean install, the 'Tip' animation may appear but will stop on its own after a few seconds.",
- use = true
+ use = false
) {
compatibleWith("com.instagram.android")
@@ -31,4 +31,4 @@ val disableReelsScrollingPatch = bytecodePatch(
// Return false in onInterceptTouchEvent to disable pull-to-refresh.
clipsSwipeRefreshLayoutOnInterceptTouchEventFingerprint.method.returnEarly(false)
}
-}
\ No newline at end of file
+}
From eecc44b9567bf2ca72ac99e0dafa483a6803c0f9 Mon Sep 17 00:00:00 2001
From: scruz
Date: Sun, 18 Jan 2026 20:51:59 +0530
Subject: [PATCH 33/42] fix(Boost for Reddit - Fix missing audio in video
downloads): Make it work again by reflecting Reddits latest changes (#6500)
---
.../fix/downloads/FixAudioMissingInDownloadsPatch.kt | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/boostforreddit/fix/downloads/FixAudioMissingInDownloadsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/boostforreddit/fix/downloads/FixAudioMissingInDownloadsPatch.kt
index 8cb3f5518..40c23a76c 100644
--- a/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/boostforreddit/fix/downloads/FixAudioMissingInDownloadsPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/boostforreddit/fix/downloads/FixAudioMissingInDownloadsPatch.kt
@@ -14,8 +14,8 @@ val fixAudioMissingInDownloadsPatch = bytecodePatch(
execute {
val endpointReplacements = mapOf(
- "/DASH_audio.mp4" to "/DASH_AUDIO_128.mp4",
- "/audio" to "/DASH_AUDIO_64.mp4",
+ "/DASH_audio.mp4" to "/CMAF_AUDIO_128.mp4",
+ "/audio" to "/CMAF_AUDIO_64.mp4",
)
downloadAudioFingerprint.method.apply {
From a10c51f160dd0c8a88b9c603c217cc36040645c5 Mon Sep 17 00:00:00 2001
From: Pun Butrach
Date: Mon, 19 Jan 2026 15:38:25 +0700
Subject: [PATCH 34/42] ci: Include require environment variables in release
step (#6493)
---
.github/workflows/release.yml | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 70790b6f6..28f152e2c 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -31,7 +31,7 @@ jobs:
- name: Build
env:
- ORG_GRADLE_PROJECT_githubPackagesUsername: ${{ env.GITHUB_ACTOR }}
+ ORG_GRADLE_PROJECT_githubPackagesUsername: ${{ github.actor }}
ORG_GRADLE_PROJECT_githubPackagesPassword: ${{ secrets.GITHUB_TOKEN }}
run: ./gradlew :patches:buildAndroid clean
@@ -56,6 +56,8 @@ jobs:
id: release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ ORG_GRADLE_PROJECT_githubPackagesUsername: ${{ github.actor }}
+ ORG_GRADLE_PROJECT_githubPackagesPassword: ${{ secrets.GITHUB_TOKEN }}
- name: Attest
if: steps.release.outputs.new_release_published == 'true'
From 8b6360e34fbc823267e4e2ea4c899e1fbe65cebc Mon Sep 17 00:00:00 2001
From: semantic-release-bot
Date: Mon, 19 Jan 2026 08:40:58 +0000
Subject: [PATCH 35/42] chore: Release v5.48.0-dev.10 [skip ci]
# [5.48.0-dev.10](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.9...v5.48.0-dev.10) (2026-01-19)
### Bug Fixes
* **Boost for Reddit - Fix missing audio in video downloads:** Make it work again by reflecting Reddits latest changes ([#6500](https://github.com/ReVanced/revanced-patches/issues/6500)) ([eecc44b](https://github.com/ReVanced/revanced-patches/commit/eecc44b9567bf2ca72ac99e0dafa483a6803c0f9))
* **Instagram:** `Sanitize sharing links` ([#6483](https://github.com/ReVanced/revanced-patches/issues/6483)) ([8724759](https://github.com/ReVanced/revanced-patches/commit/87247590de3db74680cb02ba1d87bf683b2269e2))
### Features
* **Instagram:** Disable `Disable Reels scrolling` by default ([3401467](https://github.com/ReVanced/revanced-patches/commit/3401467a6d49fc75b6757a15e5c848330c1b7307))
* **Strava:** Add `Add media download` patch ([#6449](https://github.com/ReVanced/revanced-patches/issues/6449)) ([778d13c](https://github.com/ReVanced/revanced-patches/commit/778d13ce8b28ca6df3a665530320e4a21a27ae44))
* **YouTube:** Add `Pause on audio interrupt` patch ([#6464](https://github.com/ReVanced/revanced-patches/issues/6464)) ([19f146c](https://github.com/ReVanced/revanced-patches/commit/19f146c01dc381b3cccd61e61ba4901872ff12d8))
---
CHANGELOG.md | 15 +++++++++++++++
gradle.properties | 2 +-
2 files changed, 16 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b70fb7efc..84f250f21 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,18 @@
+# [5.48.0-dev.10](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.9...v5.48.0-dev.10) (2026-01-19)
+
+
+### Bug Fixes
+
+* **Boost for Reddit - Fix missing audio in video downloads:** Make it work again by reflecting Reddits latest changes ([#6500](https://github.com/ReVanced/revanced-patches/issues/6500)) ([eecc44b](https://github.com/ReVanced/revanced-patches/commit/eecc44b9567bf2ca72ac99e0dafa483a6803c0f9))
+* **Instagram:** `Sanitize sharing links` ([#6483](https://github.com/ReVanced/revanced-patches/issues/6483)) ([8724759](https://github.com/ReVanced/revanced-patches/commit/87247590de3db74680cb02ba1d87bf683b2269e2))
+
+
+### Features
+
+* **Instagram:** Disable `Disable Reels scrolling` by default ([3401467](https://github.com/ReVanced/revanced-patches/commit/3401467a6d49fc75b6757a15e5c848330c1b7307))
+* **Strava:** Add `Add media download` patch ([#6449](https://github.com/ReVanced/revanced-patches/issues/6449)) ([778d13c](https://github.com/ReVanced/revanced-patches/commit/778d13ce8b28ca6df3a665530320e4a21a27ae44))
+* **YouTube:** Add `Pause on audio interrupt` patch ([#6464](https://github.com/ReVanced/revanced-patches/issues/6464)) ([19f146c](https://github.com/ReVanced/revanced-patches/commit/19f146c01dc381b3cccd61e61ba4901872ff12d8))
+
# [5.48.0-dev.9](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.8...v5.48.0-dev.9) (2026-01-08)
diff --git a/gradle.properties b/gradle.properties
index 816fb3e47..19dd21f2e 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -3,4 +3,4 @@ org.gradle.jvmargs = -Xms512M -Xmx2048M
org.gradle.parallel = true
android.useAndroidX = true
kotlin.code.style = official
-version = 5.48.0-dev.9
+version = 5.48.0-dev.10
From 8725a49ba3a06fee0280ffcf4be62cd960cd301e Mon Sep 17 00:00:00 2001
From: Swakshan <56347042+Swakshan@users.noreply.github.com>
Date: Mon, 19 Jan 2026 14:22:36 +0530
Subject: [PATCH 36/42] feat(Instagram): Add `Hide highlights tray` patch
(#6489)
Co-authored-by: oSumAtrIX
---
patches/api/patches.api | 4 +++
.../instagram/hide/explore/HideExploreFeed.kt | 26 ++-----------------
.../hide/highlightsTray/Fingerprints.kt | 9 +++++++
.../highlightsTray/HideHighlightsTrayPatch.kt | 17 ++++++++++++
.../hide/suggestions/HideSuggestedContent.kt | 4 +--
.../patches/instagram/shared/Utils.kt | 25 ++++++++++++++++++
6 files changed, 59 insertions(+), 26 deletions(-)
create mode 100644 patches/src/main/kotlin/app/revanced/patches/instagram/hide/highlightsTray/Fingerprints.kt
create mode 100644 patches/src/main/kotlin/app/revanced/patches/instagram/hide/highlightsTray/HideHighlightsTrayPatch.kt
create mode 100644 patches/src/main/kotlin/app/revanced/patches/instagram/shared/Utils.kt
diff --git a/patches/api/patches.api b/patches/api/patches.api
index c14ba4a53..3c7dc2f1e 100644
--- a/patches/api/patches.api
+++ b/patches/api/patches.api
@@ -308,6 +308,10 @@ public final class app/revanced/patches/instagram/hide/explore/HideExploreFeedKt
public static final fun getHideExploreFeedPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
+public final class app/revanced/patches/instagram/hide/highlightsTray/HideHighlightsTrayPatchKt {
+ public static final fun getHideHighlightsTrayPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
+}
+
public final class app/revanced/patches/instagram/hide/navigation/HideNavigationButtonsKt {
public static final fun getHideNavigationButtonsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
diff --git a/patches/src/main/kotlin/app/revanced/patches/instagram/hide/explore/HideExploreFeed.kt b/patches/src/main/kotlin/app/revanced/patches/instagram/hide/explore/HideExploreFeed.kt
index a2c7d5ba5..f9ec6505f 100644
--- a/patches/src/main/kotlin/app/revanced/patches/instagram/hide/explore/HideExploreFeed.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/instagram/hide/explore/HideExploreFeed.kt
@@ -1,29 +1,7 @@
package app.revanced.patches.instagram.hide.explore
-import app.revanced.patcher.Fingerprint
-import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
-import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
-import app.revanced.patcher.patch.BytecodePatchContext
import app.revanced.patcher.patch.bytecodePatch
-import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
-
-context(BytecodePatchContext)
-internal fun Fingerprint.replaceJsonFieldWithBogus(
- key: String,
-) {
- val targetStringIndex = stringMatches!!.first { match -> match.string == key }.index
- val targetStringRegister = method.getInstruction(targetStringIndex).registerA
-
- /**
- * Replacing the JSON key we want to skip with a random string that is not a valid JSON key.
- * This way the feeds array will never be populated.
- * Received JSON keys that are not handled are simply ignored, so there are no side effects.
- */
- method.replaceInstruction(
- targetStringIndex,
- "const-string v$targetStringRegister, \"BOGUS\"",
- )
-}
+import app.revanced.patches.instagram.shared.replaceStringWithBogus
@Suppress("unused")
val hideExploreFeedPatch = bytecodePatch(
@@ -34,6 +12,6 @@ val hideExploreFeedPatch = bytecodePatch(
compatibleWith("com.instagram.android")
execute {
- exploreResponseJsonParserFingerprint.replaceJsonFieldWithBogus(EXPLORE_KEY_TO_BE_HIDDEN)
+ exploreResponseJsonParserFingerprint.replaceStringWithBogus(EXPLORE_KEY_TO_BE_HIDDEN)
}
}
diff --git a/patches/src/main/kotlin/app/revanced/patches/instagram/hide/highlightsTray/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/instagram/hide/highlightsTray/Fingerprints.kt
new file mode 100644
index 000000000..9a0341ee6
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/instagram/hide/highlightsTray/Fingerprints.kt
@@ -0,0 +1,9 @@
+package app.revanced.patches.instagram.hide.highlightsTray
+
+import app.revanced.patcher.fingerprint
+
+internal const val TARGET_STRING = "highlights_tray"
+
+internal val highlightsUrlBuilderFingerprint = fingerprint {
+ strings(TARGET_STRING,"X-IG-Accept-Hint")
+}
diff --git a/patches/src/main/kotlin/app/revanced/patches/instagram/hide/highlightsTray/HideHighlightsTrayPatch.kt b/patches/src/main/kotlin/app/revanced/patches/instagram/hide/highlightsTray/HideHighlightsTrayPatch.kt
new file mode 100644
index 000000000..3b777d9ed
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/instagram/hide/highlightsTray/HideHighlightsTrayPatch.kt
@@ -0,0 +1,17 @@
+package app.revanced.patches.instagram.hide.highlightsTray
+
+import app.revanced.patcher.patch.bytecodePatch
+import app.revanced.patches.instagram.shared.replaceStringWithBogus
+
+@Suppress("unused")
+val hideHighlightsTrayPatch = bytecodePatch(
+ name = "Hide highlights tray",
+ description = "Hides the highlights tray in profile section.",
+ use = false
+) {
+ compatibleWith("com.instagram.android")
+
+ execute {
+ highlightsUrlBuilderFingerprint.replaceStringWithBogus(TARGET_STRING)
+ }
+}
diff --git a/patches/src/main/kotlin/app/revanced/patches/instagram/hide/suggestions/HideSuggestedContent.kt b/patches/src/main/kotlin/app/revanced/patches/instagram/hide/suggestions/HideSuggestedContent.kt
index 0c2501411..9dc6dbf59 100644
--- a/patches/src/main/kotlin/app/revanced/patches/instagram/hide/suggestions/HideSuggestedContent.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/instagram/hide/suggestions/HideSuggestedContent.kt
@@ -1,7 +1,7 @@
package app.revanced.patches.instagram.hide.suggestions
import app.revanced.patcher.patch.bytecodePatch
-import app.revanced.patches.instagram.hide.explore.replaceJsonFieldWithBogus
+import app.revanced.patches.instagram.shared.replaceStringWithBogus
@Suppress("unused")
val hideSuggestedContent = bytecodePatch(
@@ -13,7 +13,7 @@ val hideSuggestedContent = bytecodePatch(
execute {
FEED_ITEM_KEYS_TO_BE_HIDDEN.forEach { key ->
- feedItemParseFromJsonFingerprint.replaceJsonFieldWithBogus(key)
+ feedItemParseFromJsonFingerprint.replaceStringWithBogus(key)
}
}
}
diff --git a/patches/src/main/kotlin/app/revanced/patches/instagram/shared/Utils.kt b/patches/src/main/kotlin/app/revanced/patches/instagram/shared/Utils.kt
new file mode 100644
index 000000000..522257aa8
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/instagram/shared/Utils.kt
@@ -0,0 +1,25 @@
+package app.revanced.patches.instagram.shared
+
+import app.revanced.patcher.Fingerprint
+import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
+import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
+import app.revanced.patcher.patch.BytecodePatchContext
+import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
+
+context(BytecodePatchContext)
+internal fun Fingerprint.replaceStringWithBogus(
+ targetString: String,
+) {
+ val targetStringIndex = stringMatches!!.first { match -> match.string == targetString }.index
+ val targetStringRegister = method.getInstruction(targetStringIndex).registerA
+
+ /**
+ * Replaces the 'target string' with 'BOGUS'.
+ * This is usually done when we need to override a JSON key or url,
+ * to skip with a random string that is not a valid JSON key.
+ */
+ method.replaceInstruction(
+ targetStringIndex,
+ "const-string v$targetStringRegister, \"BOGUS\"",
+ )
+}
\ No newline at end of file
From 7cef24a5e966178336736ecb128a397e4e1abafe Mon Sep 17 00:00:00 2001
From: semantic-release-bot
Date: Mon, 19 Jan 2026 08:56:06 +0000
Subject: [PATCH 37/42] chore: Release v5.48.0-dev.11 [skip ci]
# [5.48.0-dev.11](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.10...v5.48.0-dev.11) (2026-01-19)
### Features
* **Instagram:** Add `Hide highlights tray` patch ([#6489](https://github.com/ReVanced/revanced-patches/issues/6489)) ([8725a49](https://github.com/ReVanced/revanced-patches/commit/8725a49ba3a06fee0280ffcf4be62cd960cd301e))
---
CHANGELOG.md | 7 +++++++
gradle.properties | 2 +-
2 files changed, 8 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 84f250f21..d7d058344 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,10 @@
+# [5.48.0-dev.11](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.10...v5.48.0-dev.11) (2026-01-19)
+
+
+### Features
+
+* **Instagram:** Add `Hide highlights tray` patch ([#6489](https://github.com/ReVanced/revanced-patches/issues/6489)) ([8725a49](https://github.com/ReVanced/revanced-patches/commit/8725a49ba3a06fee0280ffcf4be62cd960cd301e))
+
# [5.48.0-dev.10](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.9...v5.48.0-dev.10) (2026-01-19)
diff --git a/gradle.properties b/gradle.properties
index 19dd21f2e..90ef58109 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -3,4 +3,4 @@ org.gradle.jvmargs = -Xms512M -Xmx2048M
org.gradle.parallel = true
android.useAndroidX = true
kotlin.code.style = official
-version = 5.48.0-dev.10
+version = 5.48.0-dev.11
From 4c4ba1c78c9f4568a2b572f5c69e9c6c734e1a7f Mon Sep 17 00:00:00 2001
From: xehpuk
Date: Mon, 19 Jan 2026 10:00:25 +0100
Subject: [PATCH 38/42] feat(Strava): Add `Add 'Give Kudos' button to 'Group
Activity'` patch (#6475)
Co-authored-by: oSumAtrIX
---
patches/api/patches.api | 4 +
.../AddGiveGroupKudosButtonToGroupActivity.kt | 201 ++++++++++++++++++
.../patches/strava/groupkudos/Fingerprints.kt | 14 ++
3 files changed, 219 insertions(+)
create mode 100644 patches/src/main/kotlin/app/revanced/patches/strava/groupkudos/AddGiveGroupKudosButtonToGroupActivity.kt
create mode 100644 patches/src/main/kotlin/app/revanced/patches/strava/groupkudos/Fingerprints.kt
diff --git a/patches/api/patches.api b/patches/api/patches.api
index 3c7dc2f1e..bcdb1bace 100644
--- a/patches/api/patches.api
+++ b/patches/api/patches.api
@@ -1204,6 +1204,10 @@ public final class app/revanced/patches/stocard/layout/HideStoryBubblesPatchKt {
public static final fun getHideStoryBubblesPatch ()Lapp/revanced/patcher/patch/ResourcePatch;
}
+public final class app/revanced/patches/strava/groupkudos/AddGiveGroupKudosButtonToGroupActivityKt {
+ public static final fun getAddGiveGroupKudosButtonToGroupActivity ()Lapp/revanced/patcher/patch/BytecodePatch;
+}
+
public final class app/revanced/patches/strava/media/download/AddMediaDownloadPatchKt {
public static final fun getAddMediaDownloadPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
diff --git a/patches/src/main/kotlin/app/revanced/patches/strava/groupkudos/AddGiveGroupKudosButtonToGroupActivity.kt b/patches/src/main/kotlin/app/revanced/patches/strava/groupkudos/AddGiveGroupKudosButtonToGroupActivity.kt
new file mode 100644
index 000000000..adc2b69ba
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/strava/groupkudos/AddGiveGroupKudosButtonToGroupActivity.kt
@@ -0,0 +1,201 @@
+package app.revanced.patches.strava.groupkudos
+
+import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
+import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
+import app.revanced.patcher.extensions.InstructionExtensions.instructions
+import app.revanced.patcher.patch.bytecodePatch
+import app.revanced.patcher.patch.resourcePatch
+import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable
+import app.revanced.util.childElementsSequence
+import app.revanced.util.findElementByAttributeValueOrThrow
+import app.revanced.util.getReference
+import com.android.tools.smali.dexlib2.AccessFlags.*
+import com.android.tools.smali.dexlib2.Opcode
+import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation
+import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction11x
+import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction21c
+import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction31i
+import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction35c
+import com.android.tools.smali.dexlib2.iface.instruction.NarrowLiteralInstruction
+import com.android.tools.smali.dexlib2.iface.reference.TypeReference
+import com.android.tools.smali.dexlib2.immutable.ImmutableClassDef
+import com.android.tools.smali.dexlib2.immutable.ImmutableField
+import com.android.tools.smali.dexlib2.immutable.ImmutableMethod
+import com.android.tools.smali.dexlib2.immutable.ImmutableMethodParameter
+import org.w3c.dom.Element
+
+private const val VIEW_CLASS_DESCRIPTOR = "Landroid/view/View;"
+private const val ON_CLICK_LISTENER_CLASS_DESCRIPTOR = "Landroid/view/View\$OnClickListener;"
+
+private var shakeToKudosStringId = -1
+private var kudosIdId = -1
+private var leaveIdId = -1
+
+private val addGiveKudosButtonToLayoutPatch = resourcePatch {
+ fun String.toResourceId() = substring(2).toInt(16)
+
+ execute {
+ document("res/values/public.xml").use { public ->
+ fun Sequence.firstByName(name: String) = first {
+ it.getAttribute("name") == name
+ }
+
+ val publicElements = public.documentElement.childElementsSequence().filter {
+ it.tagName == "public"
+ }
+ val idElements = publicElements.filter {
+ it.getAttribute("type") == "id"
+ }
+ val stringElements = publicElements.filter {
+ it.getAttribute("type") == "string"
+ }
+
+ shakeToKudosStringId =
+ stringElements.firstByName("shake_to_kudos_dialog_title").getAttribute("id").toResourceId()
+
+ val kudosIdNode = idElements.firstByName("kudos").apply {
+ kudosIdId = getAttribute("id").toResourceId()
+ }
+
+ document("res/layout/grouped_activities_dialog_group_tab.xml").use { layout ->
+ layout.childNodes.findElementByAttributeValueOrThrow("android:id", "@id/leave_group_button_container")
+ .apply {
+ // Change from "FrameLayout".
+ layout.renameNode(this, namespaceURI, "LinearLayout")
+
+ val leaveButton = childElementsSequence().first()
+ // Get "Leave Group" button ID for bytecode matching.
+ val leaveButtonIdName = leaveButton.getAttribute("android:id").substringAfter('/')
+ leaveIdId = idElements.firstByName(leaveButtonIdName).getAttribute("id").toResourceId()
+
+ // Add surrounding padding to offset decrease on buttons.
+ setAttribute("android:paddingHorizontal", "@dimen/space_2xs")
+
+ // Place buttons next to each other with equal width.
+ val kudosButton = leaveButton.apply {
+ setAttribute("android:layout_width", "0dp")
+ setAttribute("android:layout_weight", "1")
+ // Decrease padding between buttons from "@dimen/button_large_padding" ...
+ setAttribute("android:paddingHorizontal", "@dimen/space_xs")
+ }.cloneNode(true) as Element
+ kudosButton.apply {
+ setAttribute("android:id", "@id/${kudosIdNode.getAttribute("name")}")
+ setAttribute("android:text", "@string/kudos_button")
+ }.let(::appendChild)
+
+ // Downgrade emphasis of "Leave Group" button from "primary".
+ leaveButton.setAttribute("app:emphasis", "secondary")
+ }
+ }
+ }
+ }
+}
+
+@Suppress("unused")
+val addGiveGroupKudosButtonToGroupActivity = bytecodePatch(
+ name = "Add 'Give Kudos' button to 'Group Activity'",
+ description = "Adds a button that triggers the same action as shaking your phone would."
+) {
+ compatibleWith("com.strava")
+
+ dependsOn(addGiveKudosButtonToLayoutPatch)
+
+ execute {
+ val className = initFingerprint.originalClassDef.type
+ val onClickListenerClassName = "${className.substringBeforeLast(';')}\$GiveKudosOnClickListener;"
+
+ initFingerprint.method.apply {
+ val constLeaveIdInstruction = instructions.filterIsInstance().first {
+ it.narrowLiteral == leaveIdId
+ }
+ val findViewByIdInstruction =
+ getInstruction(constLeaveIdInstruction.location.index + 1)
+ val moveViewInstruction = getInstruction(constLeaveIdInstruction.location.index + 2)
+ val checkCastButtonInstruction =
+ getInstruction(constLeaveIdInstruction.location.index + 3)
+
+ val buttonClassName = checkCastButtonInstruction.getReference()!!.type
+
+ addInstructions(
+ constLeaveIdInstruction.location.index,
+ """
+ ${constLeaveIdInstruction.opcode.name} v${constLeaveIdInstruction.registerA}, $kudosIdId
+ ${findViewByIdInstruction.opcode.name} { v${findViewByIdInstruction.registerC}, v${findViewByIdInstruction.registerD} }, ${findViewByIdInstruction.reference}
+ ${moveViewInstruction.opcode.name} v${moveViewInstruction.registerA}
+ ${checkCastButtonInstruction.opcode.name} v${checkCastButtonInstruction.registerA}, ${checkCastButtonInstruction.reference}
+ new-instance v0, $onClickListenerClassName
+ invoke-direct { v0, p0 }, $onClickListenerClassName->($className)V
+ invoke-virtual { p3, v0 }, $buttonClassName->setOnClickListener($ON_CLICK_LISTENER_CLASS_DESCRIPTOR)V
+ """
+ )
+ }
+
+ val actionHandlerMethod = actionHandlerFingerprint.match(initFingerprint.originalClassDef).method
+ val constShakeToKudosStringIndex = actionHandlerMethod.instructions.indexOfFirst {
+ it is NarrowLiteralInstruction && it.narrowLiteral == shakeToKudosStringId
+ }
+ val getSingletonInstruction = actionHandlerMethod.instructions.filterIsInstance().last {
+ it.opcode == Opcode.SGET_OBJECT && it.location.index < constShakeToKudosStringIndex
+ }
+
+ val outerThisField = ImmutableField(
+ onClickListenerClassName,
+ "outerThis",
+ className,
+ PUBLIC.value or FINAL.value or SYNTHETIC.value,
+ null,
+ listOf(),
+ setOf()
+ )
+
+ val initFieldMethod = ImmutableMethod(
+ onClickListenerClassName,
+ "",
+ listOf(ImmutableMethodParameter(className, setOf(), "outerThis")),
+ "V",
+ PUBLIC.value or SYNTHETIC.value or CONSTRUCTOR.value,
+ setOf(),
+ setOf(),
+ MutableMethodImplementation(2)
+ ).toMutable().apply {
+ addInstructions(
+ """
+ invoke-direct {p0}, Ljava/lang/Object;->()V
+ iput-object p1, p0, $outerThisField
+ return-void
+ """
+ )
+ }
+
+ val onClickMethod = ImmutableMethod(
+ onClickListenerClassName,
+ "onClick",
+ listOf(ImmutableMethodParameter(VIEW_CLASS_DESCRIPTOR, setOf(), "v")),
+ "V",
+ PUBLIC.value or FINAL.value,
+ setOf(),
+ setOf(),
+ MutableMethodImplementation(2)
+ ).toMutable().apply {
+ addInstructions(
+ """
+ sget-object p1, ${getSingletonInstruction.reference}
+ iget-object p0, p0, $outerThisField
+ invoke-virtual { p0, p1 }, ${actionHandlerFingerprint.method}
+ return-void
+ """
+ )
+ }
+
+ ImmutableClassDef(
+ onClickListenerClassName,
+ PUBLIC.value or FINAL.value or SYNTHETIC.value,
+ "Ljava/lang/Object;",
+ listOf(ON_CLICK_LISTENER_CLASS_DESCRIPTOR),
+ "ProGuard", // Same as source file name of other classes.
+ listOf(),
+ setOf(outerThisField),
+ setOf(initFieldMethod, onClickMethod)
+ ).let(classes::add)
+ }
+}
diff --git a/patches/src/main/kotlin/app/revanced/patches/strava/groupkudos/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/strava/groupkudos/Fingerprints.kt
new file mode 100644
index 000000000..f5ce604fd
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/strava/groupkudos/Fingerprints.kt
@@ -0,0 +1,14 @@
+package app.revanced.patches.strava.groupkudos
+
+import app.revanced.patcher.fingerprint
+
+internal val initFingerprint = fingerprint {
+ parameters("Lcom/strava/feed/view/modal/GroupTabFragment;" , "Z" , "Landroidx/fragment/app/FragmentManager;")
+ custom { method, _ ->
+ method.name == ""
+ }
+}
+
+internal val actionHandlerFingerprint = fingerprint {
+ strings("state")
+}
From 18c0b04f0cd1bf8cd78b05af3b8ebe3a6a5f9e48 Mon Sep 17 00:00:00 2001
From: Swakshan <56347042+Swakshan@users.noreply.github.com>
Date: Mon, 19 Jan 2026 14:31:42 +0530
Subject: [PATCH 39/42] feat(Instagram): Add `Remove build expired popup` patch
(#6488)
Co-authored-by: oSumAtrIX
---
patches/api/patches.api | 4 +++
.../removeBuildExpiredPopup/Fingerprints.kt | 12 +++++++++
.../RemoveBuildExpiredPopupPatch.kt | 27 +++++++++++++++++++
3 files changed, 43 insertions(+)
create mode 100644 patches/src/main/kotlin/app/revanced/patches/instagram/misc/removeBuildExpiredPopup/Fingerprints.kt
create mode 100644 patches/src/main/kotlin/app/revanced/patches/instagram/misc/removeBuildExpiredPopup/RemoveBuildExpiredPopupPatch.kt
diff --git a/patches/api/patches.api b/patches/api/patches.api
index bcdb1bace..52f732b31 100644
--- a/patches/api/patches.api
+++ b/patches/api/patches.api
@@ -336,6 +336,10 @@ public final class app/revanced/patches/instagram/misc/links/OpenLinksExternally
public static final fun getOpenLinksExternallyPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
+public final class app/revanced/patches/instagram/misc/removeBuildExpiredPopup/RemoveBuildExpiredPopupPatchKt {
+ public static final fun getRemoveBuildExpiredPopupPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
+}
+
public final class app/revanced/patches/instagram/misc/share/domain/ChangeLinkSharingDomainPatchKt {
public static final fun getChangeLinkSharingDomainPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
diff --git a/patches/src/main/kotlin/app/revanced/patches/instagram/misc/removeBuildExpiredPopup/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/instagram/misc/removeBuildExpiredPopup/Fingerprints.kt
new file mode 100644
index 000000000..ad4a66540
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/instagram/misc/removeBuildExpiredPopup/Fingerprints.kt
@@ -0,0 +1,12 @@
+
+package app.revanced.patches.instagram.misc.removeBuildExpiredPopup
+
+import app.revanced.patcher.fingerprint
+import app.revanced.util.literal
+
+internal const val MILLISECOND_IN_A_DAY_LITERAL = 0x5265c00L
+
+internal val appUpdateLockoutBuilderFingerprint = fingerprint {
+ strings("android.hardware.sensor.hinge_angle")
+ literal { MILLISECOND_IN_A_DAY_LITERAL }
+}
diff --git a/patches/src/main/kotlin/app/revanced/patches/instagram/misc/removeBuildExpiredPopup/RemoveBuildExpiredPopupPatch.kt b/patches/src/main/kotlin/app/revanced/patches/instagram/misc/removeBuildExpiredPopup/RemoveBuildExpiredPopupPatch.kt
new file mode 100644
index 000000000..9d19e928e
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/instagram/misc/removeBuildExpiredPopup/RemoveBuildExpiredPopupPatch.kt
@@ -0,0 +1,27 @@
+package app.revanced.patches.instagram.misc.removeBuildExpiredPopup
+
+import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
+import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
+import app.revanced.patcher.extensions.InstructionExtensions.instructions
+import app.revanced.patcher.patch.bytecodePatch
+import com.android.tools.smali.dexlib2.Opcode
+import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction
+
+@Suppress("unused")
+val removeBuildExpiredPopupPatch = bytecodePatch(
+ name = "Remove build expired popup",
+ description = "Removes the popup that appears after a while, when the app version ages.",
+) {
+ compatibleWith("com.instagram.android")
+
+ execute {
+ appUpdateLockoutBuilderFingerprint.method.apply {
+ val longToIntIndex = instructions.first { it.opcode == Opcode.LONG_TO_INT }.location.index
+ val appAgeRegister = getInstruction(longToIntIndex).registerA
+
+ // Set app age to 0 days old such that the build expired popup doesn't appear.
+ addInstruction(longToIntIndex + 1, "const v$appAgeRegister, 0x0")
+ }
+ }
+}
+
From 3762f1de08ecac7f8a024b754cf9d157d6f62d93 Mon Sep 17 00:00:00 2001
From: semantic-release-bot
Date: Mon, 19 Jan 2026 09:06:41 +0000
Subject: [PATCH 40/42] chore: Release v5.48.0-dev.12 [skip ci]
# [5.48.0-dev.12](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.11...v5.48.0-dev.12) (2026-01-19)
### Features
* **Instagram:** Add `Remove build expired popup` patch ([#6488](https://github.com/ReVanced/revanced-patches/issues/6488)) ([18c0b04](https://github.com/ReVanced/revanced-patches/commit/18c0b04f0cd1bf8cd78b05af3b8ebe3a6a5f9e48))
* **Strava:** Add `Add 'Give Kudos' button to 'Group Activity'` patch ([#6475](https://github.com/ReVanced/revanced-patches/issues/6475)) ([4c4ba1c](https://github.com/ReVanced/revanced-patches/commit/4c4ba1c78c9f4568a2b572f5c69e9c6c734e1a7f))
---
CHANGELOG.md | 8 ++++++++
gradle.properties | 2 +-
2 files changed, 9 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d7d058344..27b636ec8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,11 @@
+# [5.48.0-dev.12](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.11...v5.48.0-dev.12) (2026-01-19)
+
+
+### Features
+
+* **Instagram:** Add `Remove build expired popup` patch ([#6488](https://github.com/ReVanced/revanced-patches/issues/6488)) ([18c0b04](https://github.com/ReVanced/revanced-patches/commit/18c0b04f0cd1bf8cd78b05af3b8ebe3a6a5f9e48))
+* **Strava:** Add `Add 'Give Kudos' button to 'Group Activity'` patch ([#6475](https://github.com/ReVanced/revanced-patches/issues/6475)) ([4c4ba1c](https://github.com/ReVanced/revanced-patches/commit/4c4ba1c78c9f4568a2b572f5c69e9c6c734e1a7f))
+
# [5.48.0-dev.11](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.10...v5.48.0-dev.11) (2026-01-19)
diff --git a/gradle.properties b/gradle.properties
index 90ef58109..8373cf56c 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -3,4 +3,4 @@ org.gradle.jvmargs = -Xms512M -Xmx2048M
org.gradle.parallel = true
android.useAndroidX = true
kotlin.code.style = official
-version = 5.48.0-dev.11
+version = 5.48.0-dev.12
From 83c0127ebb8f53ab8a067758619faaac5596c145 Mon Sep 17 00:00:00 2001
From: xehpuk
Date: Mon, 19 Jan 2026 10:16:30 +0100
Subject: [PATCH 41/42] feat: Add `Prevent screenshot detection` patch (#6482)
Co-authored-by: Swakshan <56347042+Swakshan@users.noreply.github.com>
Co-authored-by: oSumAtrIX
---
patches/api/patches.api | 4 ++
.../PreventScreenshotDetectionPatch.kt | 51 +++++++++++++++++++
2 files changed, 55 insertions(+)
create mode 100644 patches/src/main/kotlin/app/revanced/patches/all/misc/screenshot/PreventScreenshotDetectionPatch.kt
diff --git a/patches/api/patches.api b/patches/api/patches.api
index 52f732b31..abe4c4027 100644
--- a/patches/api/patches.api
+++ b/patches/api/patches.api
@@ -124,6 +124,10 @@ public final class app/revanced/patches/all/misc/screencapture/RemoveScreenCaptu
public static final fun getRemoveScreenCaptureRestrictionPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
+public final class app/revanced/patches/all/misc/screenshot/PreventScreenshotDetectionPatchKt {
+ public static final fun getPreventScreenshotDetectionPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
+}
+
public final class app/revanced/patches/all/misc/screenshot/RemoveScreenshotRestrictionPatchKt {
public static final fun getRemoveScreenshotRestrictionPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
diff --git a/patches/src/main/kotlin/app/revanced/patches/all/misc/screenshot/PreventScreenshotDetectionPatch.kt b/patches/src/main/kotlin/app/revanced/patches/all/misc/screenshot/PreventScreenshotDetectionPatch.kt
new file mode 100644
index 000000000..46e9eefa8
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/all/misc/screenshot/PreventScreenshotDetectionPatch.kt
@@ -0,0 +1,51 @@
+package app.revanced.patches.all.misc.screenshot
+
+import app.revanced.patcher.extensions.InstructionExtensions.removeInstruction
+import app.revanced.patcher.patch.bytecodePatch
+import app.revanced.patches.all.misc.transformation.transformInstructionsPatch
+import app.revanced.util.getReference
+import com.android.tools.smali.dexlib2.Opcode
+import com.android.tools.smali.dexlib2.iface.reference.MethodReference
+import com.android.tools.smali.dexlib2.immutable.reference.ImmutableMethodReference
+import com.android.tools.smali.dexlib2.util.MethodUtil
+
+private val registerScreenCaptureCallbackMethodReference = ImmutableMethodReference(
+ "Landroid/app/Activity;",
+ "registerScreenCaptureCallback",
+ listOf(
+ "Ljava/util/concurrent/Executor;",
+ "Landroid/app/Activity\$ScreenCaptureCallback;",
+ ),
+ "V"
+)
+
+private val unregisterScreenCaptureCallbackMethodReference = ImmutableMethodReference(
+ "Landroid/app/Activity;",
+ "unregisterScreenCaptureCallback",
+ listOf(
+ "Landroid/app/Activity\$ScreenCaptureCallback;",
+ ),
+ "V"
+)
+
+@Suppress("unused")
+val preventScreenshotDetectionPatch = bytecodePatch(
+ name = "Prevent screenshot detection",
+ description = "Removes the registration of all screen capture callbacks. This prevents the app from detecting screenshots.",
+) {
+ dependsOn(transformInstructionsPatch(
+ filterMap = { _, _, instruction, instructionIndex ->
+ if (instruction.opcode != Opcode.INVOKE_VIRTUAL) return@transformInstructionsPatch null
+
+ val reference = instruction.getReference() ?: return@transformInstructionsPatch null
+
+ instructionIndex.takeIf {
+ MethodUtil.methodSignaturesMatch(reference, registerScreenCaptureCallbackMethodReference) ||
+ MethodUtil.methodSignaturesMatch(reference, unregisterScreenCaptureCallbackMethodReference)
+ }
+ },
+ transform = { mutableMethod, instructionIndex ->
+ mutableMethod.removeInstruction(instructionIndex)
+ }
+ ))
+}
From 182224c79dcbbabd9ee61f6066b31bf08425bb1f Mon Sep 17 00:00:00 2001
From: semantic-release-bot
Date: Mon, 19 Jan 2026 09:21:21 +0000
Subject: [PATCH 42/42] chore: Release v5.48.0-dev.13 [skip ci]
# [5.48.0-dev.13](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.12...v5.48.0-dev.13) (2026-01-19)
### Features
* Add `Prevent screenshot detection` patch ([#6482](https://github.com/ReVanced/revanced-patches/issues/6482)) ([83c0127](https://github.com/ReVanced/revanced-patches/commit/83c0127ebb8f53ab8a067758619faaac5596c145))
---
CHANGELOG.md | 7 +++++++
gradle.properties | 2 +-
2 files changed, 8 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 27b636ec8..cbdc30d96 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,10 @@
+# [5.48.0-dev.13](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.12...v5.48.0-dev.13) (2026-01-19)
+
+
+### Features
+
+* Add `Prevent screenshot detection` patch ([#6482](https://github.com/ReVanced/revanced-patches/issues/6482)) ([83c0127](https://github.com/ReVanced/revanced-patches/commit/83c0127ebb8f53ab8a067758619faaac5596c145))
+
# [5.48.0-dev.12](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.11...v5.48.0-dev.12) (2026-01-19)
diff --git a/gradle.properties b/gradle.properties
index 8373cf56c..2ae4eb89c 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -3,4 +3,4 @@ org.gradle.jvmargs = -Xms512M -Xmx2048M
org.gradle.parallel = true
android.useAndroidX = true
kotlin.code.style = official
-version = 5.48.0-dev.12
+version = 5.48.0-dev.13