diff --git a/CHANGELOG.md b/CHANGELOG.md
index ccdad47e6..5ca60dc40 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,58 @@
+# [5.23.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v5.23.0-dev.6...v5.23.0-dev.7) (2025-05-06)
+
+
+### Bug Fixes
+
+* Fix incorrect fingerprints ([#4917](https://github.com/ReVanced/revanced-patches/issues/4917)) ([49ca329](https://github.com/ReVanced/revanced-patches/commit/49ca3290a726cdba7bc9b62ffcd8d46e6f04778e))
+
+# [5.23.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v5.23.0-dev.5...v5.23.0-dev.6) (2025-05-06)
+
+
+### Bug Fixes
+
+* Correct incorrect fingerprint ([c3bab89](https://github.com/ReVanced/revanced-patches/commit/c3bab89fc4189e38c10eee0caa36289de7e29dfa))
+
+# [5.23.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.23.0-dev.4...v5.23.0-dev.5) (2025-05-06)
+
+
+### Bug Fixes
+
+* **Spotify - Unlock Spotify Premium:** Remove pop up premium ads ([#4842](https://github.com/ReVanced/revanced-patches/issues/4842)) ([00aa200](https://github.com/ReVanced/revanced-patches/commit/00aa2000ba2eef15a0dd827c2bd84c2e85c412e0))
+
+
+### Features
+
+* **Pandora:** Add `Disable audio ads` and `Unlimited skips` patch ([#4841](https://github.com/ReVanced/revanced-patches/issues/4841)) ([0cf7a4c](https://github.com/ReVanced/revanced-patches/commit/0cf7a4c6be615ed0a52a6bacf87592f5f43ff575))
+* **Prime Video:** Add `Skip ads` patch ([#4824](https://github.com/ReVanced/revanced-patches/issues/4824)) ([bb672c4](https://github.com/ReVanced/revanced-patches/commit/bb672c4674ddc201b8b2648c3906cfc31ef43f10))
+
+# [5.23.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.23.0-dev.3...v5.23.0-dev.4) (2025-05-06)
+
+
+### Features
+
+* **Spotify:** Add `Sanitize sharing links` patch ([#4829](https://github.com/ReVanced/revanced-patches/issues/4829)) ([2e3511d](https://github.com/ReVanced/revanced-patches/commit/2e3511d03c8198bbdb9336888df038a33fb3ab8c))
+
+# [5.23.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.23.0-dev.2...v5.23.0-dev.3) (2025-05-05)
+
+
+### Bug Fixes
+
+* **YouTube:** Simplify litho filtering patch ([#4910](https://github.com/ReVanced/revanced-patches/issues/4910)) ([bd53955](https://github.com/ReVanced/revanced-patches/commit/bd53955df738bb7b819eb91a3e776e9d2ca5c74a))
+
+# [5.23.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.23.0-dev.1...v5.23.0-dev.2) (2025-05-04)
+
+
+### Bug Fixes
+
+* **YouTube:** Improve litho filtering performance ([#4904](https://github.com/ReVanced/revanced-patches/issues/4904)) ([7b43986](https://github.com/ReVanced/revanced-patches/commit/7b43986871a68e5cb43331d2fb2fdb9ef67438ad))
+
+# [5.23.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.22.0...v5.23.0-dev.1) (2025-05-02)
+
+
+### Features
+
+* **Lightroom:** Constrain patches to last working version ([efef03b](https://github.com/ReVanced/revanced-patches/commit/efef03b80da21552d0d8be6913faba64e4fb5ed1))
+
# [5.22.0](https://github.com/ReVanced/revanced-patches/compare/v5.21.0...v5.22.0) (2025-05-01)
diff --git a/extensions/primevideo/build.gradle.kts b/extensions/primevideo/build.gradle.kts
new file mode 100644
index 000000000..9a81cc3e8
--- /dev/null
+++ b/extensions/primevideo/build.gradle.kts
@@ -0,0 +1,4 @@
+dependencies {
+ compileOnly(project(":extensions:shared:library"))
+ compileOnly(project(":extensions:primevideo:stub"))
+}
diff --git a/extensions/primevideo/src/main/AndroidManifest.xml b/extensions/primevideo/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..9b65eb06c
--- /dev/null
+++ b/extensions/primevideo/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+
diff --git a/extensions/primevideo/src/main/java/app/revanced/extension/primevideo/ads/SkipAdsPatch.java b/extensions/primevideo/src/main/java/app/revanced/extension/primevideo/ads/SkipAdsPatch.java
new file mode 100644
index 000000000..d0a97810a
--- /dev/null
+++ b/extensions/primevideo/src/main/java/app/revanced/extension/primevideo/ads/SkipAdsPatch.java
@@ -0,0 +1,36 @@
+package app.revanced.extension.primevideo.ads;
+
+import com.amazon.avod.fsm.SimpleTrigger;
+import com.amazon.avod.media.ads.AdBreak;
+import com.amazon.avod.media.ads.internal.state.AdBreakTrigger;
+import com.amazon.avod.media.ads.internal.state.AdEnabledPlayerTriggerType;
+import com.amazon.avod.media.playback.VideoPlayer;
+import com.amazon.avod.media.ads.internal.state.ServerInsertedAdBreakState;
+
+import app.revanced.extension.shared.Logger;
+
+@SuppressWarnings("unused")
+public final class SkipAdsPatch {
+ public static void enterServerInsertedAdBreakState(ServerInsertedAdBreakState state, AdBreakTrigger trigger, VideoPlayer player) {
+ try {
+ AdBreak adBreak = trigger.getBreak();
+
+ // There are two scenarios when entering the original method:
+ // 1. Player naturally entered an ad break while watching a video.
+ // 2. User is skipped/scrubbed to a position on the timeline. If seek position is past an ad break,
+ // user is forced to watch an ad before continuing.
+ //
+ // Scenario 2 is indicated by trigger.getSeekStartPosition() != null, so skip directly to the scrubbing
+ // target. Otherwise, just calculate when the ad break should end and skip to there.
+ if (trigger.getSeekStartPosition() != null)
+ player.seekTo(trigger.getSeekTarget().getTotalMilliseconds());
+ else
+ player.seekTo(player.getCurrentPosition() + adBreak.getDurationExcludingAux().getTotalMilliseconds());
+
+ // Send "end of ads" trigger to state machine so everything doesn't get whacky.
+ state.doTrigger(new SimpleTrigger(AdEnabledPlayerTriggerType.NO_MORE_ADS_SKIP_TRANSITION));
+ } catch (Exception ex) {
+ Logger.printException(() -> "Failed skipping ads", ex);
+ }
+ }
+}
\ No newline at end of file
diff --git a/extensions/primevideo/stub/build.gradle.kts b/extensions/primevideo/stub/build.gradle.kts
new file mode 100644
index 000000000..2d9865785
--- /dev/null
+++ b/extensions/primevideo/stub/build.gradle.kts
@@ -0,0 +1,17 @@
+plugins {
+ id(libs.plugins.android.library.get().pluginId)
+}
+
+android {
+ namespace = "app.revanced.extension"
+ compileSdk = 34
+
+ defaultConfig {
+ minSdk = 21
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_11
+ targetCompatibility = JavaVersion.VERSION_11
+ }
+}
diff --git a/extensions/primevideo/stub/src/main/AndroidManifest.xml b/extensions/primevideo/stub/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..9b65eb06c
--- /dev/null
+++ b/extensions/primevideo/stub/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+
diff --git a/extensions/primevideo/stub/src/main/java/com/amazon/avod/fsm/SimpleTrigger.java b/extensions/primevideo/stub/src/main/java/com/amazon/avod/fsm/SimpleTrigger.java
new file mode 100644
index 000000000..b537fe040
--- /dev/null
+++ b/extensions/primevideo/stub/src/main/java/com/amazon/avod/fsm/SimpleTrigger.java
@@ -0,0 +1,6 @@
+package com.amazon.avod.fsm;
+
+public final class SimpleTrigger implements Trigger {
+ public SimpleTrigger(T triggerType) {
+ }
+}
\ No newline at end of file
diff --git a/extensions/primevideo/stub/src/main/java/com/amazon/avod/fsm/StateBase.java b/extensions/primevideo/stub/src/main/java/com/amazon/avod/fsm/StateBase.java
new file mode 100644
index 000000000..95741308c
--- /dev/null
+++ b/extensions/primevideo/stub/src/main/java/com/amazon/avod/fsm/StateBase.java
@@ -0,0 +1,7 @@
+package com.amazon.avod.fsm;
+
+public abstract class StateBase {
+ // This method orginally has protected access (modified in patch code).
+ public void doTrigger(Trigger trigger) {
+ }
+}
\ No newline at end of file
diff --git a/extensions/primevideo/stub/src/main/java/com/amazon/avod/fsm/Trigger.java b/extensions/primevideo/stub/src/main/java/com/amazon/avod/fsm/Trigger.java
new file mode 100644
index 000000000..282f0f200
--- /dev/null
+++ b/extensions/primevideo/stub/src/main/java/com/amazon/avod/fsm/Trigger.java
@@ -0,0 +1,4 @@
+package com.amazon.avod.fsm;
+
+public interface Trigger {
+}
\ No newline at end of file
diff --git a/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/TimeSpan.java b/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/TimeSpan.java
new file mode 100644
index 000000000..cc90e43cd
--- /dev/null
+++ b/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/TimeSpan.java
@@ -0,0 +1,7 @@
+package com.amazon.avod.media;
+
+public final class TimeSpan {
+ public long getTotalMilliseconds() {
+ throw new UnsupportedOperationException();
+ }
+}
\ No newline at end of file
diff --git a/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/AdBreak.java b/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/AdBreak.java
new file mode 100644
index 000000000..9a950434d
--- /dev/null
+++ b/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/AdBreak.java
@@ -0,0 +1,7 @@
+package com.amazon.avod.media.ads;
+
+import com.amazon.avod.media.TimeSpan;
+
+public interface AdBreak {
+ TimeSpan getDurationExcludingAux();
+}
\ No newline at end of file
diff --git a/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/internal/state/AdBreakState.java b/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/internal/state/AdBreakState.java
new file mode 100644
index 000000000..f417660ed
--- /dev/null
+++ b/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/internal/state/AdBreakState.java
@@ -0,0 +1,4 @@
+package com.amazon.avod.media.ads.internal.state;
+
+public abstract class AdBreakState extends AdEnabledPlaybackState {
+}
\ No newline at end of file
diff --git a/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/internal/state/AdBreakTrigger.java b/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/internal/state/AdBreakTrigger.java
new file mode 100644
index 000000000..f8b399565
--- /dev/null
+++ b/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/internal/state/AdBreakTrigger.java
@@ -0,0 +1,18 @@
+package com.amazon.avod.media.ads.internal.state;
+
+import com.amazon.avod.media.ads.AdBreak;
+import com.amazon.avod.media.TimeSpan;
+
+public class AdBreakTrigger {
+ public AdBreak getBreak() {
+ throw new UnsupportedOperationException();
+ }
+
+ public TimeSpan getSeekTarget() {
+ throw new UnsupportedOperationException();
+ }
+
+ public TimeSpan getSeekStartPosition() {
+ throw new UnsupportedOperationException();
+ }
+}
\ No newline at end of file
diff --git a/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/internal/state/AdEnabledPlaybackState.java b/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/internal/state/AdEnabledPlaybackState.java
new file mode 100644
index 000000000..445aad580
--- /dev/null
+++ b/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/internal/state/AdEnabledPlaybackState.java
@@ -0,0 +1,8 @@
+package com.amazon.avod.media.ads.internal.state;
+
+import com.amazon.avod.fsm.StateBase;
+import com.amazon.avod.media.playback.state.PlayerStateType;
+import com.amazon.avod.media.playback.state.trigger.PlayerTriggerType;
+
+public class AdEnabledPlaybackState extends StateBase {
+}
\ No newline at end of file
diff --git a/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/internal/state/AdEnabledPlayerTriggerType.java b/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/internal/state/AdEnabledPlayerTriggerType.java
new file mode 100644
index 000000000..e7951e934
--- /dev/null
+++ b/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/internal/state/AdEnabledPlayerTriggerType.java
@@ -0,0 +1,5 @@
+package com.amazon.avod.media.ads.internal.state;
+
+public enum AdEnabledPlayerTriggerType {
+ NO_MORE_ADS_SKIP_TRANSITION
+}
\ No newline at end of file
diff --git a/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/internal/state/ServerInsertedAdBreakState.java b/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/internal/state/ServerInsertedAdBreakState.java
new file mode 100644
index 000000000..07c198013
--- /dev/null
+++ b/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/internal/state/ServerInsertedAdBreakState.java
@@ -0,0 +1,4 @@
+package com.amazon.avod.media.ads.internal.state;
+
+public class ServerInsertedAdBreakState extends AdBreakState {
+}
\ No newline at end of file
diff --git a/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/playback/VideoPlayer.java b/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/playback/VideoPlayer.java
new file mode 100644
index 000000000..af3d0bee5
--- /dev/null
+++ b/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/playback/VideoPlayer.java
@@ -0,0 +1,7 @@
+package com.amazon.avod.media.playback;
+
+public interface VideoPlayer {
+ long getCurrentPosition();
+
+ void seekTo(long positionMs);
+}
\ No newline at end of file
diff --git a/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/playback/state/PlayerStateType.java b/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/playback/state/PlayerStateType.java
new file mode 100644
index 000000000..202723285
--- /dev/null
+++ b/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/playback/state/PlayerStateType.java
@@ -0,0 +1,4 @@
+package com.amazon.avod.media.playback.state;
+
+public interface PlayerStateType {
+}
\ No newline at end of file
diff --git a/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/playback/state/trigger/PlayerTriggerType.java b/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/playback/state/trigger/PlayerTriggerType.java
new file mode 100644
index 000000000..eac139f9b
--- /dev/null
+++ b/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/playback/state/trigger/PlayerTriggerType.java
@@ -0,0 +1,4 @@
+package com.amazon.avod.media.playback.state.trigger;
+
+public interface PlayerTriggerType {
+}
\ No newline at end of file
diff --git a/extensions/spotify/src/main/java/app/revanced/extension/spotify/misc/UnlockPremiumPatch.java b/extensions/spotify/src/main/java/app/revanced/extension/spotify/misc/UnlockPremiumPatch.java
index 9d5907811..f9371db44 100644
--- a/extensions/spotify/src/main/java/app/revanced/extension/spotify/misc/UnlockPremiumPatch.java
+++ b/extensions/spotify/src/main/java/app/revanced/extension/spotify/misc/UnlockPremiumPatch.java
@@ -130,6 +130,7 @@ public final class UnlockPremiumPatch {
/**
* Injection point. Remove ads sections from home.
+ * Depends on patching protobuffer list remove method.
*/
public static void removeHomeSections(List sections) {
try {
diff --git a/extensions/spotify/src/main/java/app/revanced/extension/spotify/misc/privacy/SanitizeSharingLinksPatch.java b/extensions/spotify/src/main/java/app/revanced/extension/spotify/misc/privacy/SanitizeSharingLinksPatch.java
new file mode 100644
index 000000000..55541ec9c
--- /dev/null
+++ b/extensions/spotify/src/main/java/app/revanced/extension/spotify/misc/privacy/SanitizeSharingLinksPatch.java
@@ -0,0 +1,43 @@
+package app.revanced.extension.spotify.misc.privacy;
+
+import android.net.Uri;
+
+import java.util.List;
+
+import app.revanced.extension.shared.Logger;
+
+@SuppressWarnings("unused")
+public final class SanitizeSharingLinksPatch {
+
+ /**
+ * Parameters that are considered undesirable and should be stripped away.
+ */
+ private static final List SHARE_PARAMETERS_TO_REMOVE = List.of(
+ "si", // Share tracking parameter.
+ "utm_source" // Share source, such as "copy-link".
+ );
+
+ /**
+ * Injection point.
+ */
+ public static String sanitizeUrl(String url) {
+ try {
+ Uri uri = Uri.parse(url);
+ Uri.Builder builder = uri.buildUpon().clearQuery();
+
+ for (String paramName : uri.getQueryParameterNames()) {
+ if (!SHARE_PARAMETERS_TO_REMOVE.contains(paramName)) {
+ for (String value : uri.getQueryParameters(paramName)) {
+ builder.appendQueryParameter(paramName, value);
+ }
+ }
+ }
+
+ return builder.build().toString();
+ } catch (Exception ex) {
+ Logger.printException(() -> "sanitizeUrl failure", ex);
+
+ return url;
+ }
+ }
+}
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/LithoFilterPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/LithoFilterPatch.java
index 1edd27509..3b054c6e6 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/LithoFilterPatch.java
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/LithoFilterPatch.java
@@ -87,6 +87,10 @@ public final class LithoFilterPatch {
* the buffer is saved to a ThreadLocal so each calling thread does not interfere with other threads.
*/
private static final ThreadLocal bufferThreadLocal = new ThreadLocal<>();
+ /**
+ * Results of calling {@link #filter(String, StringBuilder)}.
+ */
+ private static final ThreadLocal filterResult = new ThreadLocal<>();
static {
for (Filter filter : filters) {
@@ -140,11 +144,22 @@ public final class LithoFilterPatch {
}
}
+ /**
+ * Injection point.
+ */
+ public static boolean shouldFilter() {
+ Boolean shouldFilter = filterResult.get();
+ return shouldFilter != null && shouldFilter;
+ }
+
/**
* Injection point. Called off the main thread, and commonly called by multiple threads at the same time.
*/
- @SuppressWarnings("unused")
- public static boolean filter(@Nullable String lithoIdentifier, @NonNull StringBuilder pathBuilder) {
+ public static void filter(@Nullable String lithoIdentifier, StringBuilder pathBuilder) {
+ filterResult.set(handleFiltering(lithoIdentifier, pathBuilder));
+ }
+
+ private static boolean handleFiltering(@Nullable String lithoIdentifier, StringBuilder pathBuilder) {
try {
if (pathBuilder.length() == 0) {
return false;
diff --git a/gradle.properties b/gradle.properties
index ce1b08bf3..6f68428b1 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.22.0
+version = 5.23.0-dev.7
diff --git a/patches/api/patches.api b/patches/api/patches.api
index 7d32bec85..7e9a01a9c 100644
--- a/patches/api/patches.api
+++ b/patches/api/patches.api
@@ -380,6 +380,14 @@ public final class app/revanced/patches/openinghours/misc/fix/crash/FixCrashPatc
public static final fun getFixCrashPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
+public final class app/revanced/patches/pandora/ads/DisableAudioAdsPatchKt {
+ public static final fun getDisableAudioAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
+}
+
+public final class app/revanced/patches/pandora/misc/EnableUnlimitedSkipsPatchKt {
+ public static final fun getEnableUnlimitedSkipsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
+}
+
public final class app/revanced/patches/photomath/detection/deviceid/SpoofDeviceIdPatchKt {
public static final fun getGetDeviceIdPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
@@ -412,6 +420,14 @@ public final class app/revanced/patches/pixiv/ads/HideAdsPatchKt {
public static final fun getHideAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
+public final class app/revanced/patches/primevideo/ads/SkipAdsPatchKt {
+ public static final fun getSkipAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
+}
+
+public final class app/revanced/patches/primevideo/misc/extension/ExtensionPatchKt {
+ public static final fun getSharedExtensionPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
+}
+
public final class app/revanced/patches/protonmail/signature/RemoveSentFromSignaturePatchKt {
public static final fun getRemoveSentFromSignaturePatch ()Lapp/revanced/patcher/patch/ResourcePatch;
}
@@ -852,6 +868,10 @@ public final class app/revanced/patches/spotify/misc/fix/SpoofSignaturePatchKt {
public static final fun getSpoofSignaturePatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
+public final class app/revanced/patches/spotify/misc/privacy/SanitizeSharingLinksPatchKt {
+ public static final fun getSanitizeSharingLinksPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
+}
+
public final class app/revanced/patches/spotify/navbar/PremiumNavbarTabPatchKt {
public static final fun getPremiumNavbarTabPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
@@ -1534,6 +1554,7 @@ public final class app/revanced/patches/yuka/misc/unlockpremium/UnlockPremiumPat
public final class app/revanced/util/BytecodeUtilsKt {
public static final fun addInstructionsAtControlFlowLabel (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;ILjava/lang/String;)V
+ public static final fun addInstructionsAtControlFlowLabel (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;ILjava/lang/String;[Lapp/revanced/patcher/util/smali/ExternalLabel;)V
public static final fun containsLiteralInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;D)Z
public static final fun containsLiteralInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;F)Z
public static final fun containsLiteralInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;J)Z
diff --git a/patches/src/main/kotlin/app/revanced/patches/lightroom/misc/login/DisableMandatoryLoginPatch.kt b/patches/src/main/kotlin/app/revanced/patches/lightroom/misc/login/DisableMandatoryLoginPatch.kt
index a5a1cf8f3..1ba1c2048 100644
--- a/patches/src/main/kotlin/app/revanced/patches/lightroom/misc/login/DisableMandatoryLoginPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/lightroom/misc/login/DisableMandatoryLoginPatch.kt
@@ -7,7 +7,7 @@ import app.revanced.patcher.patch.bytecodePatch
val disableMandatoryLoginPatch = bytecodePatch(
name = "Disable mandatory login",
) {
- compatibleWith("com.adobe.lrmobile")
+ compatibleWith("com.adobe.lrmobile"("10.0.2"))
execute {
isLoggedInFingerprint.method.apply {
diff --git a/patches/src/main/kotlin/app/revanced/patches/lightroom/misc/premium/UnlockPremiumPatch.kt b/patches/src/main/kotlin/app/revanced/patches/lightroom/misc/premium/UnlockPremiumPatch.kt
index b9187af27..4561cee41 100644
--- a/patches/src/main/kotlin/app/revanced/patches/lightroom/misc/premium/UnlockPremiumPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/lightroom/misc/premium/UnlockPremiumPatch.kt
@@ -7,7 +7,7 @@ import app.revanced.patcher.patch.bytecodePatch
val unlockPremiumPatch = bytecodePatch(
name = "Unlock premium",
) {
- compatibleWith("com.adobe.lrmobile")
+ compatibleWith("com.adobe.lrmobile"("10.0.2"))
execute {
// Set hasPremium = true.
diff --git a/patches/src/main/kotlin/app/revanced/patches/pandora/ads/DisableAudioAdsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/pandora/ads/DisableAudioAdsPatch.kt
new file mode 100644
index 000000000..a25a8880a
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/pandora/ads/DisableAudioAdsPatch.kt
@@ -0,0 +1,30 @@
+package app.revanced.patches.pandora.ads
+
+import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
+import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
+import app.revanced.patcher.patch.bytecodePatch
+import app.revanced.patches.pandora.shared.constructUserDataFingerprint
+import app.revanced.util.indexOfFirstInstructionOrThrow
+import com.android.tools.smali.dexlib2.Opcode
+import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
+
+@Suppress("unused")
+val disableAudioAdsPatch = bytecodePatch(
+ name = "Disable audio ads",
+) {
+ compatibleWith("com.pandora.android")
+
+ execute {
+ constructUserDataFingerprint.method.apply {
+ // First match is "hasAudioAds".
+ val hasAudioAdsStringIndex = constructUserDataFingerprint.stringMatches!!.first().index
+ val moveResultIndex = indexOfFirstInstructionOrThrow(hasAudioAdsStringIndex, Opcode.MOVE_RESULT)
+ val hasAudioAdsRegister = getInstruction(moveResultIndex).registerA
+
+ addInstruction(
+ moveResultIndex + 1,
+ "const/4 v$hasAudioAdsRegister, 0"
+ )
+ }
+ }
+}
diff --git a/patches/src/main/kotlin/app/revanced/patches/pandora/misc/EnableUnlimitedSkipsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/pandora/misc/EnableUnlimitedSkipsPatch.kt
new file mode 100644
index 000000000..aedb5a9c2
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/pandora/misc/EnableUnlimitedSkipsPatch.kt
@@ -0,0 +1,31 @@
+package app.revanced.patches.pandora.misc
+
+import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
+import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
+import app.revanced.patcher.patch.bytecodePatch
+import app.revanced.patches.pandora.shared.constructUserDataFingerprint
+import app.revanced.util.indexOfFirstInstructionOrThrow
+import com.android.tools.smali.dexlib2.Opcode
+import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
+
+@Suppress("unused")
+val enableUnlimitedSkipsPatch = bytecodePatch(
+ name = "Enable unlimited skips",
+) {
+ compatibleWith("com.pandora.android")
+
+ execute {
+ constructUserDataFingerprint.method.apply {
+ // Last match is "skipLimitBehavior".
+ val skipLimitBehaviorStringIndex = constructUserDataFingerprint.stringMatches!!.last().index
+ val moveResultObjectIndex =
+ indexOfFirstInstructionOrThrow(skipLimitBehaviorStringIndex, Opcode.MOVE_RESULT_OBJECT)
+ val skipLimitBehaviorRegister = getInstruction(moveResultObjectIndex).registerA
+
+ addInstruction(
+ moveResultObjectIndex + 1,
+ "const-string v$skipLimitBehaviorRegister, \"unlimited\""
+ )
+ }
+ }
+}
diff --git a/patches/src/main/kotlin/app/revanced/patches/pandora/shared/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/pandora/shared/Fingerprints.kt
new file mode 100644
index 000000000..c045e0841
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/pandora/shared/Fingerprints.kt
@@ -0,0 +1,7 @@
+package app.revanced.patches.pandora.shared
+
+import app.revanced.patcher.fingerprint
+
+internal val constructUserDataFingerprint = fingerprint {
+ strings("hasAudioAds", "skipLimitBehavior")
+}
diff --git a/patches/src/main/kotlin/app/revanced/patches/primevideo/ads/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/primevideo/ads/Fingerprints.kt
new file mode 100644
index 000000000..ac3a1c43a
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/primevideo/ads/Fingerprints.kt
@@ -0,0 +1,33 @@
+package app.revanced.patches.primevideo.ads
+
+import app.revanced.patcher.fingerprint
+import com.android.tools.smali.dexlib2.AccessFlags
+import com.android.tools.smali.dexlib2.Opcode
+
+internal val enterServerInsertedAdBreakStateFingerprint = fingerprint {
+ accessFlags(AccessFlags.PUBLIC)
+ parameters("Lcom/amazon/avod/fsm/Trigger;")
+ returns("V")
+ opcodes(
+ Opcode.INVOKE_VIRTUAL,
+ Opcode.MOVE_RESULT_OBJECT,
+ Opcode.CONST_4,
+ Opcode.CONST_4
+ )
+ custom { method, classDef ->
+ method.name == "enter" && classDef.type == "Lcom/amazon/avod/media/ads/internal/state/ServerInsertedAdBreakState;"
+ }
+}
+
+internal val doTriggerFingerprint = fingerprint {
+ accessFlags(AccessFlags.PROTECTED)
+ returns("V")
+ opcodes(
+ Opcode.IGET_OBJECT,
+ Opcode.INVOKE_INTERFACE,
+ Opcode.RETURN_VOID
+ )
+ custom { method, classDef ->
+ method.name == "doTrigger" && classDef.type == "Lcom/amazon/avod/fsm/StateBase;"
+ }
+}
\ No newline at end of file
diff --git a/patches/src/main/kotlin/app/revanced/patches/primevideo/ads/SkipAdsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/primevideo/ads/SkipAdsPatch.kt
new file mode 100644
index 000000000..e43828932
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/primevideo/ads/SkipAdsPatch.kt
@@ -0,0 +1,45 @@
+package app.revanced.patches.primevideo.ads
+
+import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
+import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
+import app.revanced.patcher.patch.bytecodePatch
+import app.revanced.patches.primevideo.misc.extension.sharedExtensionPatch
+import com.android.tools.smali.dexlib2.AccessFlags
+import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
+
+@Suppress("unused")
+val skipAdsPatch = bytecodePatch(
+ name = "Skip ads",
+ description = "Automatically skips video stream ads.",
+) {
+ compatibleWith("com.amazon.avod.thirdpartyclient"("3.0.403.257"))
+
+ dependsOn(sharedExtensionPatch)
+
+ // Skip all the logic in ServerInsertedAdBreakState.enter(), which plays all the ad clips in this
+ // ad break. Instead, force the video player to seek over the entire break and reset the state machine.
+ execute {
+ // Force doTrigger() access to public so we can call it from our extension.
+ doTriggerFingerprint.method.accessFlags = AccessFlags.PUBLIC.value;
+
+ val getPlayerIndex = enterServerInsertedAdBreakStateFingerprint.patternMatch!!.startIndex
+ enterServerInsertedAdBreakStateFingerprint.method.apply {
+ // Get register that stores VideoPlayer:
+ // invoke-virtual ->getPrimaryPlayer()
+ // move-result-object { playerRegister }
+ val playerRegister = getInstruction(getPlayerIndex + 1).registerA
+
+ // Reuse the params from the original method:
+ // p0 = ServerInsertedAdBreakState
+ // p1 = AdBreakTrigger
+ addInstructions(
+ getPlayerIndex + 2,
+ """
+ invoke-static { p0, p1, v$playerRegister }, Lapp/revanced/extension/primevideo/ads/SkipAdsPatch;->enterServerInsertedAdBreakState(Lcom/amazon/avod/media/ads/internal/state/ServerInsertedAdBreakState;Lcom/amazon/avod/media/ads/internal/state/AdBreakTrigger;Lcom/amazon/avod/media/playback/VideoPlayer;)V
+ return-void
+ """
+ )
+ }
+ }
+}
+
diff --git a/patches/src/main/kotlin/app/revanced/patches/primevideo/misc/extension/ExtensionPatch.kt b/patches/src/main/kotlin/app/revanced/patches/primevideo/misc/extension/ExtensionPatch.kt
new file mode 100644
index 000000000..34f4f9b36
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/primevideo/misc/extension/ExtensionPatch.kt
@@ -0,0 +1,5 @@
+package app.revanced.patches.primevideo.misc.extension
+
+import app.revanced.patches.shared.misc.extension.sharedExtensionPatch
+
+val sharedExtensionPatch = sharedExtensionPatch("primevideo", applicationInitHook)
diff --git a/patches/src/main/kotlin/app/revanced/patches/primevideo/misc/extension/Hooks.kt b/patches/src/main/kotlin/app/revanced/patches/primevideo/misc/extension/Hooks.kt
new file mode 100644
index 000000000..763c2bfd5
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/primevideo/misc/extension/Hooks.kt
@@ -0,0 +1,9 @@
+package app.revanced.patches.primevideo.misc.extension
+
+import app.revanced.patches.shared.misc.extension.extensionHook
+
+internal val applicationInitHook = extensionHook {
+ custom { method, classDef ->
+ method.name == "onCreate" && classDef.endsWith("/SplashScreenActivity;")
+ }
+}
diff --git a/patches/src/main/kotlin/app/revanced/patches/spotify/misc/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/Fingerprints.kt
index c6fe5fcec..daddd2841 100644
--- a/patches/src/main/kotlin/app/revanced/patches/spotify/misc/Fingerprints.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/Fingerprints.kt
@@ -2,8 +2,12 @@ package app.revanced.patches.spotify.misc
import app.revanced.patcher.fingerprint
import app.revanced.patches.spotify.misc.extension.IS_SPOTIFY_LEGACY_APP_TARGET
+import app.revanced.util.getReference
+import app.revanced.util.indexOfFirstInstruction
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
+import com.android.tools.smali.dexlib2.iface.reference.FieldReference
+import com.android.tools.smali.dexlib2.iface.reference.TypeReference
internal val accountAttributeFingerprint = fingerprint {
custom { _, classDef ->
@@ -15,7 +19,7 @@ internal val accountAttributeFingerprint = fingerprint {
}
}
-internal val productStateProtoFingerprint = fingerprint {
+internal val productStateProtoGetMapFingerprint = fingerprint {
returns("Ljava/util/Map;")
custom { _, classDef ->
classDef.type == if (IS_SPOTIFY_LEGACY_APP_TARGET) {
@@ -56,16 +60,40 @@ internal val readPlayerOptionOverridesFingerprint = fingerprint {
}
}
-internal val homeSectionFingerprint = fingerprint {
- custom { _, classDef -> classDef.endsWith("homeapi/proto/Section;") }
-}
-
internal val protobufListsFingerprint = fingerprint {
accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC)
custom { method, _ -> method.name == "emptyProtobufList" }
}
-internal val homeStructureFingerprint = fingerprint {
- opcodes(Opcode.IGET_OBJECT, Opcode.RETURN_OBJECT)
- custom { _, classDef -> classDef.endsWith("homeapi/proto/HomeStructure;") }
+internal val protobufListRemoveFingerprint = fingerprint {
+ custom { method, _ -> method.name == "remove" }
}
+
+internal val homeSectionFingerprint = fingerprint {
+ custom { _, classDef -> classDef.endsWith("homeapi/proto/Section;") }
+}
+
+internal val homeStructureGetSectionsFingerprint = fingerprint {
+ custom { method, classDef ->
+ classDef.endsWith("homeapi/proto/HomeStructure;") && method.indexOfFirstInstruction {
+ opcode == Opcode.IGET_OBJECT && getReference()?.name == "sections_"
+ } >= 0
+ }
+}
+
+internal fun reactivexFunctionApplyWithClassInitFingerprint(className: String) = fingerprint {
+ returns("Ljava/lang/Object;")
+ parameters("Ljava/lang/Object;")
+ custom { method, _ -> method.name == "apply" && method.indexOfFirstInstruction {
+ opcode == Opcode.NEW_INSTANCE && getReference()?.type?.endsWith(className) == true
+ } >= 0
+ }
+}
+
+internal const val PENDRAGON_JSON_FETCH_MESSAGE_REQUEST_CLASS_NAME = "FetchMessageRequest;"
+internal val pendragonJsonFetchMessageRequestFingerprint =
+ reactivexFunctionApplyWithClassInitFingerprint(PENDRAGON_JSON_FETCH_MESSAGE_REQUEST_CLASS_NAME)
+
+internal const val PENDRAGON_PROTO_FETCH_MESSAGE_LIST_REQUEST_CLASS_NAME = "FetchMessageListRequest;"
+internal val pendragonProtoFetchMessageListRequestFingerprint =
+ reactivexFunctionApplyWithClassInitFingerprint(PENDRAGON_PROTO_FETCH_MESSAGE_LIST_REQUEST_CLASS_NAME)
diff --git a/patches/src/main/kotlin/app/revanced/patches/spotify/misc/UnlockPremiumPatch.kt b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/UnlockPremiumPatch.kt
index 8678517f9..90bfc1694 100644
--- a/patches/src/main/kotlin/app/revanced/patches/spotify/misc/UnlockPremiumPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/UnlockPremiumPatch.kt
@@ -4,22 +4,25 @@ import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
import app.revanced.patcher.extensions.InstructionExtensions.removeInstruction
+import app.revanced.patcher.extensions.InstructionExtensions.removeInstructions
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
-import app.revanced.patcher.fingerprint
+import app.revanced.patcher.patch.PatchException
import app.revanced.patcher.patch.bytecodePatch
-import app.revanced.patches.spotify.misc.check.checkEnvironmentPatch
+import app.revanced.patcher.util.proxy.mutableTypes.MutableClass
+import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
import app.revanced.patches.spotify.misc.extension.IS_SPOTIFY_LEGACY_APP_TARGET
import app.revanced.patches.spotify.misc.extension.sharedExtensionPatch
import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstructionOrThrow
import app.revanced.util.indexOfFirstInstructionReversedOrThrow
-import com.android.tools.smali.dexlib2.AccessFlags
+import app.revanced.util.toPublicAccessFlags
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
+import com.android.tools.smali.dexlib2.iface.reference.TypeReference
import java.util.logging.Logger
private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/spotify/misc/UnlockPremiumPatch;"
@@ -42,14 +45,18 @@ val unlockPremiumPatch = bytecodePatch(
)
execute {
- // Make _value accessible so that it can be overridden in the extension.
- accountAttributeFingerprint.classDef.fields.first { it.name == "value_" }.apply {
- // Add public flag and remove private.
- accessFlags = accessFlags.or(AccessFlags.PUBLIC.value).and(AccessFlags.PRIVATE.value.inv())
+ fun MutableClass.publicizeField(fieldName: String) {
+ fields.first { it.name == fieldName }.apply {
+ // Add public and remove private flag.
+ accessFlags = accessFlags.toPublicAccessFlags()
+ }
}
+ // Make _value accessible so that it can be overridden in the extension.
+ accountAttributeFingerprint.classDef.publicizeField("value_")
+
// Override the attributes map in the getter method.
- productStateProtoFingerprint.method.apply {
+ productStateProtoGetMapFingerprint.method.apply {
val getAttributesMapIndex = indexOfFirstInstructionOrThrow(Opcode.IGET_OBJECT)
val attributesMapRegister = getInstruction(getAttributesMapIndex).registerA
@@ -62,19 +69,20 @@ val unlockPremiumPatch = bytecodePatch(
// Add the query parameter trackRows to show popular tracks in the artist page.
- buildQueryParametersFingerprint.apply {
- val addQueryParameterConditionIndex = method.indexOfFirstInstructionReversedOrThrow(
- stringMatches!!.first().index, Opcode.IF_EQZ
+ buildQueryParametersFingerprint.method.apply {
+ val addQueryParameterConditionIndex = indexOfFirstInstructionReversedOrThrow(
+ buildQueryParametersFingerprint.stringMatches!!.first().index, Opcode.IF_EQZ
)
- method.replaceInstruction(addQueryParameterConditionIndex, "nop")
+ replaceInstruction(addQueryParameterConditionIndex, "nop")
}
if (IS_SPOTIFY_LEGACY_APP_TARGET) {
- return@execute Logger.getLogger(this::class.java.name).warning(
+ Logger.getLogger(this::class.java.name).warning(
"Patching a legacy Spotify version. Patch functionality may be limited."
)
+ return@execute
}
@@ -105,48 +113,39 @@ val unlockPremiumPatch = bytecodePatch(
val shufflingContextCallIndex = indexOfFirstInstructionOrThrow {
getReference()?.name == "shufflingContext"
}
+ val boolRegister = getInstruction(shufflingContextCallIndex).registerD
- val registerBool = getInstruction(shufflingContextCallIndex).registerD
addInstruction(
shufflingContextCallIndex,
- "sget-object v$registerBool, Ljava/lang/Boolean;->FALSE:Ljava/lang/Boolean;"
+ "sget-object v$boolRegister, Ljava/lang/Boolean;->FALSE:Ljava/lang/Boolean;"
)
}
// Disable the "Spotify Premium" upsell experiment in context menus.
- contextMenuExperimentsFingerprint.apply {
- val moveIsEnabledIndex = method.indexOfFirstInstructionOrThrow(
- stringMatches!!.first().index, Opcode.MOVE_RESULT
+ contextMenuExperimentsFingerprint.method.apply {
+ val moveIsEnabledIndex = indexOfFirstInstructionOrThrow(
+ contextMenuExperimentsFingerprint.stringMatches!!.first().index, Opcode.MOVE_RESULT
)
- val isUpsellEnabledRegister = method.getInstruction(moveIsEnabledIndex).registerA
+ val isUpsellEnabledRegister = getInstruction(moveIsEnabledIndex).registerA
- method.replaceInstruction(moveIsEnabledIndex, "const/4 v$isUpsellEnabledRegister, 0")
+ replaceInstruction(moveIsEnabledIndex, "const/4 v$isUpsellEnabledRegister, 0")
}
- // Make featureTypeCase_ accessible so we can check the home section type in the extension.
- homeSectionFingerprint.classDef.fields.first { it.name == "featureTypeCase_" }.apply {
- // Add public flag and remove private.
- accessFlags = accessFlags.or(AccessFlags.PUBLIC.value).and(AccessFlags.PRIVATE.value.inv())
- }
-
- val protobufListClassName = with(protobufListsFingerprint.originalMethod) {
+ val protobufListClassDef = with(protobufListsFingerprint.originalMethod) {
val emptyProtobufListGetIndex = indexOfFirstInstructionOrThrow(Opcode.SGET_OBJECT)
- getInstruction(emptyProtobufListGetIndex).getReference()!!.definingClass
- }
+ // Find the protobuffer list class using the definingClass which contains the empty list static value.
+ val classType = getInstruction(emptyProtobufListGetIndex).getReference()!!.definingClass
- val protobufListRemoveFingerprint = fingerprint {
- custom { method, classDef ->
- method.name == "remove" && classDef.type == protobufListClassName
- }
+ classes.find { it.type == classType } ?: throw PatchException("Could not find protobuffer list class.")
}
// Need to allow mutation of the list so the home ads sections can be removed.
// Protobuffer list has an 'isMutable' boolean parameter that sets the mutability.
// Forcing that always on breaks unrelated code in strange ways.
// Instead, remove the method call that checks if the list is unmodifiable.
- protobufListRemoveFingerprint.method.apply {
+ protobufListRemoveFingerprint.match(protobufListClassDef).method.apply {
val invokeThrowUnmodifiableIndex = indexOfFirstInstructionOrThrow {
val reference = getReference()
opcode == Opcode.INVOKE_VIRTUAL &&
@@ -157,8 +156,12 @@ val unlockPremiumPatch = bytecodePatch(
removeInstruction(invokeThrowUnmodifiableIndex)
}
+
+ // Make featureTypeCase_ accessible so we can check the home section type in the extension.
+ homeSectionFingerprint.classDef.publicizeField("featureTypeCase_")
+
// Remove ads sections from home.
- homeStructureFingerprint.method.apply {
+ homeStructureGetSectionsFingerprint.method.apply {
val getSectionsIndex = indexOfFirstInstructionOrThrow(Opcode.IGET_OBJECT)
val sectionsRegister = getInstruction(getSectionsIndex).registerA
@@ -168,5 +171,56 @@ val unlockPremiumPatch = bytecodePatch(
"$EXTENSION_CLASS_DESCRIPTOR->removeHomeSections(Ljava/util/List;)V"
)
}
+
+
+ // Replace a fetch request that returns and maps Singles with their static onErrorReturn value.
+ fun MutableMethod.replaceFetchRequestSingleWithError(requestClassName: String) {
+ // The index of where the request class is being instantiated.
+ val requestInstantiationIndex = indexOfFirstInstructionOrThrow {
+ getReference()?.type?.endsWith(requestClassName) == true
+ }
+
+ // The index of where the onErrorReturn method is called with the error static value.
+ val onErrorReturnCallIndex = indexOfFirstInstructionOrThrow(requestInstantiationIndex) {
+ getReference()?.name == "onErrorReturn"
+ }
+ val onErrorReturnCallInstruction = getInstruction(onErrorReturnCallIndex)
+
+ // The error static value register.
+ val onErrorReturnValueRegister = onErrorReturnCallInstruction.registerD
+
+ // The index where the error static value starts being constructed.
+ // Because the Singles are mapped, the error static value starts being constructed right after the first
+ // move-result-object of the map call, before the onErrorReturn method call.
+ val onErrorReturnValueConstructionIndex =
+ indexOfFirstInstructionReversedOrThrow(onErrorReturnCallIndex, Opcode.MOVE_RESULT_OBJECT) + 1
+
+ val singleClassName = onErrorReturnCallInstruction.getReference()!!.definingClass
+ // The index where the request is firstly called, before its result is mapped to other values.
+ val requestCallIndex = indexOfFirstInstructionOrThrow(requestInstantiationIndex) {
+ getReference()?.returnType == singleClassName
+ }
+
+ // Construct a new single with the error static value and return it.
+ addInstructions(
+ onErrorReturnCallIndex,
+ "invoke-static { v$onErrorReturnValueRegister }, " +
+ "$singleClassName->just(Ljava/lang/Object;)$singleClassName\n" +
+ "move-result-object v$onErrorReturnValueRegister\n" +
+ "return-object v$onErrorReturnValueRegister"
+ )
+
+ // Remove every instruction from the request call to right before the error static value construction.
+ val removeCount = onErrorReturnValueConstructionIndex - requestCallIndex
+ removeInstructions(requestCallIndex, removeCount)
+ }
+
+ // Remove pendragon (pop up ads) requests and return the errors instead.
+ pendragonJsonFetchMessageRequestFingerprint.method.replaceFetchRequestSingleWithError(
+ PENDRAGON_JSON_FETCH_MESSAGE_REQUEST_CLASS_NAME
+ )
+ pendragonProtoFetchMessageListRequestFingerprint.method.replaceFetchRequestSingleWithError(
+ PENDRAGON_PROTO_FETCH_MESSAGE_LIST_REQUEST_CLASS_NAME
+ )
}
}
diff --git a/patches/src/main/kotlin/app/revanced/patches/spotify/misc/fix/SpoofPackageInfoPatch.kt b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/fix/SpoofPackageInfoPatch.kt
index 2836a4872..58606777d 100644
--- a/patches/src/main/kotlin/app/revanced/patches/spotify/misc/fix/SpoofPackageInfoPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/fix/SpoofPackageInfoPatch.kt
@@ -60,4 +60,4 @@ val spoofPackageInfoPatch = bytecodePatch(
// endregion
}
}
-}
\ No newline at end of file
+}
diff --git a/patches/src/main/kotlin/app/revanced/patches/spotify/misc/privacy/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/privacy/Fingerprints.kt
new file mode 100644
index 000000000..3d60abf9b
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/privacy/Fingerprints.kt
@@ -0,0 +1,41 @@
+package app.revanced.patches.spotify.misc.privacy
+
+import app.revanced.patcher.fingerprint
+import app.revanced.util.literal
+import com.android.tools.smali.dexlib2.AccessFlags
+
+internal val shareCopyUrlFingerprint = fingerprint {
+ returns("Ljava/lang/Object;")
+ parameters("Ljava/lang/Object;")
+ strings("clipboard", "Spotify Link")
+ custom { method, _ ->
+ method.name == "invokeSuspend"
+ }
+}
+
+internal val shareCopyUrlLegacyFingerprint = fingerprint {
+ returns("Ljava/lang/Object;")
+ parameters("Ljava/lang/Object;")
+ strings("clipboard", "createNewSession failed")
+ custom { method, _ ->
+ method.name == "apply"
+ }
+}
+
+internal val formatAndroidShareSheetUrlFingerprint = fingerprint {
+ accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC)
+ returns("Ljava/lang/String;")
+ parameters("L", "Ljava/lang/String;")
+ literal {
+ '\n'.code.toLong()
+ }
+}
+
+internal val formatAndroidShareSheetUrlLegacyFingerprint = fingerprint {
+ accessFlags(AccessFlags.PUBLIC)
+ returns("Ljava/lang/String;")
+ parameters("Lcom/spotify/share/social/sharedata/ShareData;", "Ljava/lang/String;")
+ literal {
+ '\n'.code.toLong()
+ }
+}
diff --git a/patches/src/main/kotlin/app/revanced/patches/spotify/misc/privacy/SanitizeSharingLinksPatch.kt b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/privacy/SanitizeSharingLinksPatch.kt
new file mode 100644
index 000000000..8df4c7720
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/privacy/SanitizeSharingLinksPatch.kt
@@ -0,0 +1,70 @@
+package app.revanced.patches.spotify.misc.privacy
+
+import app.revanced.patcher.Fingerprint
+import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
+import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
+import app.revanced.patcher.patch.bytecodePatch
+import app.revanced.patches.spotify.misc.extension.IS_SPOTIFY_LEGACY_APP_TARGET
+import app.revanced.patches.spotify.misc.extension.sharedExtensionPatch
+import app.revanced.util.getReference
+import app.revanced.util.indexOfFirstInstructionOrThrow
+import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction
+import com.android.tools.smali.dexlib2.iface.reference.MethodReference
+
+private const val EXTENSION_CLASS_DESCRIPTOR =
+ "Lapp/revanced/extension/spotify/misc/privacy/SanitizeSharingLinksPatch;"
+
+@Suppress("unused")
+val sanitizeSharingLinksPatch = bytecodePatch(
+ name = "Sanitize sharing links",
+ description = "Removes the tracking query parameters from links before they are shared.",
+) {
+ compatibleWith("com.spotify.music")
+
+ dependsOn(sharedExtensionPatch)
+
+ execute {
+ val extensionMethodDescriptor = "$EXTENSION_CLASS_DESCRIPTOR->" +
+ "sanitizeUrl(Ljava/lang/String;)Ljava/lang/String;"
+
+ val copyFingerprint = if (IS_SPOTIFY_LEGACY_APP_TARGET) {
+ shareCopyUrlLegacyFingerprint
+ } else {
+ shareCopyUrlFingerprint
+ }
+
+ copyFingerprint.method.apply {
+ val newPlainTextInvokeIndex = indexOfFirstInstructionOrThrow {
+ getReference()?.name == "newPlainText"
+ }
+ val register = getInstruction(newPlainTextInvokeIndex).registerD
+
+ addInstructions(
+ newPlainTextInvokeIndex,
+ """
+ invoke-static { v$register }, $extensionMethodDescriptor
+ move-result-object v$register
+ """
+ )
+ }
+
+ // Android native share sheet is used for all other quick share types (X, WhatsApp, etc).
+ val shareUrlParameter : String
+ val shareSheetFingerprint : Fingerprint
+ if (IS_SPOTIFY_LEGACY_APP_TARGET) {
+ shareSheetFingerprint = formatAndroidShareSheetUrlLegacyFingerprint
+ shareUrlParameter = "p2"
+ } else {
+ shareSheetFingerprint = formatAndroidShareSheetUrlFingerprint
+ shareUrlParameter = "p1"
+ }
+
+ shareSheetFingerprint.method.addInstructions(
+ 0,
+ """
+ invoke-static { $shareUrlParameter }, $extensionMethodDescriptor
+ move-result-object $shareUrlParameter
+ """
+ )
+ }
+}
diff --git a/patches/src/main/kotlin/app/revanced/patches/spotify/shared/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/spotify/shared/Fingerprints.kt
index 14149ac7a..1afbcde45 100644
--- a/patches/src/main/kotlin/app/revanced/patches/spotify/shared/Fingerprints.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/spotify/shared/Fingerprints.kt
@@ -14,4 +14,4 @@ internal val mainActivityOnCreateFingerprint = fingerprint {
method.name == "onCreate" && (classDef.type == SPOTIFY_MAIN_ACTIVITY
|| classDef.type == SPOTIFY_MAIN_ACTIVITY_LEGACY)
}
-}
\ No newline at end of file
+}
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/returnyoutubedislike/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/returnyoutubedislike/Fingerprints.kt
index 3943d58ca..54fda75c8 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/returnyoutubedislike/Fingerprints.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/returnyoutubedislike/Fingerprints.kt
@@ -5,18 +5,6 @@ import app.revanced.util.literal
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
-internal val conversionContextFingerprint = fingerprint {
- returns("Ljava/lang/String;")
- parameters()
- strings(
- ", widthConstraint=",
- ", heightConstraint=",
- ", templateLoggerFactory=",
- ", rootDisposableContainer=",
- "ConversionContext{containerInternal=",
- )
-}
-
internal val dislikeFingerprint = fingerprint {
returns("V")
strings("like/dislike")
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/returnyoutubedislike/ReturnYouTubeDislikePatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/returnyoutubedislike/ReturnYouTubeDislikePatch.kt
index fc01bf804..1f3316396 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/returnyoutubedislike/ReturnYouTubeDislikePatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/returnyoutubedislike/ReturnYouTubeDislikePatch.kt
@@ -18,6 +18,7 @@ import app.revanced.patches.youtube.misc.playservice.versionCheckPatch
import app.revanced.patches.youtube.misc.settings.addSettingPreference
import app.revanced.patches.youtube.misc.settings.newIntent
import app.revanced.patches.youtube.misc.settings.settingsPatch
+import app.revanced.patches.youtube.shared.conversionContextFingerprintToString
import app.revanced.patches.youtube.shared.rollingNumberTextViewAnimationUpdateFingerprint
import app.revanced.patches.youtube.video.videoid.hookPlayerResponseVideoId
import app.revanced.patches.youtube.video.videoid.hookVideoId
@@ -113,11 +114,11 @@ val returnYouTubeDislikePatch = bytecodePatch(
// This hook handles all situations, as it's where the created Spans are stored and later reused.
// Find the field name of the conversion context.
val conversionContextField = textComponentConstructorFingerprint.originalClassDef.fields.find {
- it.type == conversionContextFingerprint.originalClassDef.type
+ it.type == conversionContextFingerprintToString.originalClassDef.type
} ?: throw PatchException("Could not find conversion context field")
textComponentLookupFingerprint.match(textComponentConstructorFingerprint.originalClassDef)
- textComponentLookupFingerprint.method.apply {
+ .method.apply {
// Find the instruction for creating the text data object.
val textDataClassType = textComponentDataFingerprint.originalClassDef.type
@@ -160,12 +161,12 @@ val returnYouTubeDislikePatch = bytecodePatch(
addInstructionsAtControlFlowLabel(
insertIndex,
"""
- # Copy conversion context
- move-object/from16 v$tempRegister, p0
- iget-object v$tempRegister, v$tempRegister, $conversionContextField
- invoke-static { v$tempRegister, v$charSequenceRegister }, $EXTENSION_CLASS_DESCRIPTOR->onLithoTextLoaded(Ljava/lang/Object;Ljava/lang/CharSequence;)Ljava/lang/CharSequence;
- move-result-object v$charSequenceRegister
- """,
+ # Copy conversion context
+ move-object/from16 v$tempRegister, p0
+ iget-object v$tempRegister, v$tempRegister, $conversionContextField
+ invoke-static { v$tempRegister, v$charSequenceRegister }, $EXTENSION_CLASS_DESCRIPTOR->onLithoTextLoaded(Ljava/lang/Object;Ljava/lang/CharSequence;)Ljava/lang/CharSequence;
+ move-result-object v$charSequenceRegister
+ """
)
}
@@ -201,11 +202,9 @@ val returnYouTubeDislikePatch = bytecodePatch(
val charSequenceFieldReference =
getInstruction(dislikesIndex).reference
- val registerCount = implementation!!.registerCount
+ val conversionContextRegister = implementation!!.registerCount - parameters.size + 1
- // This register is being overwritten, so it is free to use.
- val freeRegister = registerCount - 1
- val conversionContextRegister = registerCount - parameters.size + 1
+ val freeRegister = findFreeRegister(insertIndex, charSequenceInstanceRegister, conversionContextRegister)
addInstructions(
insertIndex,
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/litho/filter/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/litho/filter/Fingerprints.kt
index 89db01f0e..d14955e9c 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/litho/filter/Fingerprints.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/litho/filter/Fingerprints.kt
@@ -5,10 +5,6 @@ import app.revanced.util.literal
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
-/**
- * In 19.17 and earlier, this resolves to the same method as [readComponentIdentifierFingerprint].
- * In 19.18+ this resolves to a different method.
- */
internal val componentContextParserFingerprint = fingerprint {
strings(
"TreeNode result must be set.",
@@ -17,11 +13,21 @@ internal val componentContextParserFingerprint = fingerprint {
)
}
+/**
+ * Resolves to the class found in [componentContextParserFingerprint].
+ * When patching 19.16 this fingerprint matches the same method as [componentContextParserFingerprint].
+ */
+internal val componentContextSubParserFingerprint = fingerprint {
+ strings(
+ "Number of bits must be positive"
+ )
+}
+
internal val lithoFilterFingerprint = fingerprint {
accessFlags(AccessFlags.STATIC, AccessFlags.CONSTRUCTOR)
returns("V")
custom { _, classDef ->
- classDef.endsWith("LithoFilterPatch;")
+ classDef.endsWith("/LithoFilterPatch;")
}
}
@@ -37,14 +43,6 @@ internal val protobufBufferReferenceFingerprint = fingerprint {
)
}
-/**
-* In 19.17 and earlier, this resolves to the same method as [componentContextParserFingerprint].
-* In 19.18+ this resolves to a different method.
-*/
-internal val readComponentIdentifierFingerprint = fingerprint {
- strings("Number of bits must be positive")
-}
-
internal val emptyComponentFingerprint = fingerprint {
accessFlags(AccessFlags.PRIVATE, AccessFlags.CONSTRUCTOR)
parameters()
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/litho/filter/LithoFilterPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/litho/filter/LithoFilterPatch.kt
index 1ede2fe5d..f8b8d05d8 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/litho/filter/LithoFilterPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/litho/filter/LithoFilterPatch.kt
@@ -4,25 +4,25 @@ package app.revanced.patches.youtube.misc.litho.filter
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.removeInstructions
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
import app.revanced.patcher.patch.bytecodePatch
-import app.revanced.patcher.util.smali.ExternalLabel
import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch
-import app.revanced.patches.youtube.misc.playservice.is_19_18_or_greater
import app.revanced.patches.youtube.misc.playservice.is_19_25_or_greater
import app.revanced.patches.youtube.misc.playservice.is_20_05_or_greater
import app.revanced.patches.youtube.misc.playservice.versionCheckPatch
+import app.revanced.patches.youtube.shared.conversionContextFingerprintToString
+import app.revanced.util.addInstructionsAtControlFlowLabel
import app.revanced.util.findFreeRegister
+import app.revanced.util.findInstructionIndicesReversedOrThrow
import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstructionOrThrow
import app.revanced.util.indexOfFirstInstructionReversedOrThrow
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
-import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction
+import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
@@ -53,42 +53,33 @@ val lithoFilterPatch = bytecodePatch(
* The buffer is a large byte array that represents the component tree.
* This byte array is searched for strings that indicate the current component.
*
- * The following pseudocode shows how the patch works:
+ * All modifications done here must allow all the original code to still execute
+ * even when filtering, otherwise memory leaks or poor app performance may occur.
+ *
+ * The following pseudocode shows how this patch works:
*
* class SomeOtherClass {
- * // Called before ComponentContextParser.parseBytesToComponentContext method.
+ * // Called before ComponentContextParser.parseComponent() method.
* public void someOtherMethod(ByteBuffer byteBuffer) {
* ExtensionClass.setProtoBuffer(byteBuffer); // Inserted by this patch.
* ...
* }
* }
*
- * When patching 19.17 and earlier:
- *
* class ComponentContextParser {
- * public ComponentContext ReadComponentIdentifierFingerprint(...) {
+ * public Component parseComponent() {
* ...
- * if (extensionClass.filter(identifier, pathBuilder)); // Inserted by this patch.
+ *
+ * // Checks if the component should be filtered.
+ * // Sets a thread local with the filtering result.
+ * extensionClass.filter(identifier, pathBuilder); // Inserted by this patch.
+ *
+ * ...
+ *
+ * if (extensionClass.shouldFilter()) { // Inserted by this patch.
* return emptyComponent;
- * ...
- * }
- * }
- *
- * When patching 19.18 and later:
- *
- * class ComponentContextParser {
- * public ComponentContext parseBytesToComponentContext(...) {
- * ...
- * if (ReadComponentIdentifierFingerprint() == null); // Inserted by this patch.
- * return emptyComponent;
- * ...
- * }
- *
- * public ComponentIdentifierObj readComponentIdentifier(...) {
- * ...
- * if (extensionClass.filter(identifier, pathBuilder)); // Inserted by this patch.
- * return null;
- * ...
+ * }
+ * return originalUnpatchedComponent; // Original code.
* }
* }
*/
@@ -103,7 +94,7 @@ val lithoFilterPatch = bytecodePatch(
2,
"""
new-instance v1, $classDescriptor
- invoke-direct {v1}, $classDescriptor->()V
+ invoke-direct { v1 }, $classDescriptor->()V
const/16 v2, ${filterCount++}
aput-object v1, v0, v2
""",
@@ -115,110 +106,105 @@ val lithoFilterPatch = bytecodePatch(
protobufBufferReferenceFingerprint.method.addInstruction(
0,
- " invoke-static { p2 }, $EXTENSION_CLASS_DESCRIPTOR->setProtoBuffer(Ljava/nio/ByteBuffer;)V",
+ "invoke-static { p2 }, $EXTENSION_CLASS_DESCRIPTOR->setProtoBuffer(Ljava/nio/ByteBuffer;)V",
)
// endregion
// region Hook the method that parses bytes into a ComponentContext.
- val readComponentMethod = readComponentIdentifierFingerprint.originalMethod
- // Get the only static method in the class.
- val builderMethodDescriptor = emptyComponentFingerprint.classDef.methods.first { method ->
- AccessFlags.STATIC.isSet(method.accessFlags)
- }
- // Only one field.
- val emptyComponentField = classBy { classDef ->
- builderMethodDescriptor.returnType == classDef.type
- }!!.immutableClass.fields.single()
-
- // Returns an empty component instead of the original component.
- fun createReturnEmptyComponentInstructions(register: Int): String =
- """
- move-object/from16 v$register, p1
- invoke-static { v$register }, $builderMethodDescriptor
- move-result-object v$register
- iget-object v$register, v$register, $emptyComponentField
- return-object v$register
- """
-
+ // Allow the method to run to completion, and override the
+ // return value with an empty component if it should be filtered.
+ // It is important to allow the original code to always run to completion,
+ // otherwise memory leaks and poor app performance can occur.
+ //
+ // The extension filtering result needs to be saved off somewhere, but cannot
+ // save to a class field since the target class is called by multiple threads.
+ // It would be great if there was a way to change the register count of the
+ // method implementation and save the result to a high register to later use
+ // in the method, but there is no simple way to do that.
+ // Instead save the extension filter result to a thread local and check the
+ // filtering result at each method return index.
+ // String field for the litho identifier.
componentContextParserFingerprint.method.apply {
- // 19.18 and later require patching 2 methods instead of one.
- // Otherwise the modifications done here are the same for all targets.
- if (is_19_18_or_greater) {
- // Get the method name of the ReadComponentIdentifierFingerprint call.
- val readComponentMethodCallIndex = indexOfFirstInstructionOrThrow {
- val reference = getReference()
- reference?.definingClass == readComponentMethod.definingClass &&
- reference.name == readComponentMethod.name
+ val conversionContextClass = conversionContextFingerprintToString.originalClassDef
+
+ val conversionContextIdentifierField = componentContextSubParserFingerprint.match(
+ componentContextParserFingerprint.originalClassDef
+ ).let {
+ // Identifier field is loaded just before the string declaration.
+ val index = it.method.indexOfFirstInstructionReversedOrThrow(
+ it.stringMatches!!.first().index
+ ) {
+ val reference = getReference()
+ reference?.definingClass == conversionContextClass.type
+ && reference.type == "Ljava/lang/String;"
}
-
- // Result of read component, and also a free register.
- val register = getInstruction(readComponentMethodCallIndex + 1).registerA
-
- // Insert after 'move-result-object'
- val insertHookIndex = readComponentMethodCallIndex + 2
-
- // Return an EmptyComponent instead of the original component if the filterState method returns true.
- addInstructionsWithLabels(
- insertHookIndex,
- """
- if-nez v$register, :unfiltered
-
- # Component was filtered in ReadComponentIdentifierFingerprint hook
- ${createReturnEmptyComponentInstructions(register)}
- """,
- ExternalLabel("unfiltered", getInstruction(insertHookIndex)),
- )
+ it.method.getInstruction(index).getReference()
}
- }
- // endregion
+ // StringBuilder field for the litho path.
+ val conversionContextPathBuilderField = conversionContextClass.fields
+ .single { field -> field.type == "Ljava/lang/StringBuilder;" }
- // region Read component then store the result.
+ val conversionContextResultIndex = indexOfFirstInstructionOrThrow {
+ val reference = getReference()
+ reference?.returnType == conversionContextClass.type
+ } + 1
- readComponentIdentifierFingerprint.method.apply {
- val insertHookIndex = indexOfFirstInstructionOrThrow {
- opcode == Opcode.IPUT_OBJECT &&
- getReference()?.type == "Ljava/lang/StringBuilder;"
- }
- val stringBuilderRegister = getInstruction(insertHookIndex).registerA
-
- // Identifier is saved to a field just before the string builder.
- val identifierRegister = getInstruction(
- indexOfFirstInstructionReversedOrThrow(insertHookIndex) {
- opcode == Opcode.IPUT_OBJECT &&
- getReference()?.type == "Ljava/lang/String;"
- },
+ val conversionContextResultRegister = getInstruction(
+ conversionContextResultIndex
).registerA
- val freeRegister = findFreeRegister(insertHookIndex, identifierRegister, stringBuilderRegister)
- val invokeFilterInstructions = """
- invoke-static { v$identifierRegister, v$stringBuilderRegister }, $EXTENSION_CLASS_DESCRIPTOR->filter(Ljava/lang/String;Ljava/lang/StringBuilder;)Z
- move-result v$freeRegister
- if-eqz v$freeRegister, :unfiltered
- """
-
- addInstructionsWithLabels(
- insertHookIndex,
- if (is_19_18_or_greater) {
- """
- $invokeFilterInstructions
-
- # Return null, and the ComponentContextParserFingerprint hook
- # handles returning an empty component.
- const/4 v$freeRegister, 0x0
- return-object v$freeRegister
- """
- } else {
- """
- $invokeFilterInstructions
-
- ${createReturnEmptyComponentInstructions(freeRegister)}
- """
- },
- ExternalLabel("unfiltered", getInstruction(insertHookIndex)),
+ val identifierRegister = findFreeRegister(
+ conversionContextResultIndex, conversionContextResultRegister
)
+ val stringBuilderRegister = findFreeRegister(
+ conversionContextResultIndex, conversionContextResultRegister, identifierRegister
+ )
+
+ // Check if the component should be filtered, and save the result to a thread local.
+ addInstructionsAtControlFlowLabel(
+ conversionContextResultIndex + 1,
+ """
+ iget-object v$identifierRegister, v$conversionContextResultRegister, $conversionContextIdentifierField
+ iget-object v$stringBuilderRegister, v$conversionContextResultRegister, $conversionContextPathBuilderField
+ invoke-static { v$identifierRegister, v$stringBuilderRegister }, $EXTENSION_CLASS_DESCRIPTOR->filter(Ljava/lang/String;Ljava/lang/StringBuilder;)V
+ """
+ )
+
+ // Get the only static method in the class.
+ val builderMethodDescriptor = emptyComponentFingerprint.classDef.methods.single {
+ method -> AccessFlags.STATIC.isSet(method.accessFlags)
+ }
+ // Only one field.
+ val emptyComponentField = classBy { classDef ->
+ classDef.type == builderMethodDescriptor.returnType
+ }!!.immutableClass.fields.single()
+
+ // Check at each return value if the component is filtered,
+ // and return an empty component if filtering is needed.
+ findInstructionIndicesReversedOrThrow(Opcode.RETURN_OBJECT).forEach { returnIndex ->
+ val freeRegister = findFreeRegister(returnIndex)
+
+ addInstructionsAtControlFlowLabel(
+ returnIndex,
+ """
+ invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->shouldFilter()Z
+ move-result v$freeRegister
+ if-eqz v$freeRegister, :unfiltered
+
+ move-object/from16 v$freeRegister, p1
+ invoke-static { v$freeRegister }, $builderMethodDescriptor
+ move-result-object v$freeRegister
+ iget-object v$freeRegister, v$freeRegister, $emptyComponentField
+ return-object v$freeRegister
+
+ :unfiltered
+ nop
+ """
+ )
+ }
}
// endregion
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/shared/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/shared/Fingerprints.kt
index d0c7d30a2..29f6ea4c7 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/shared/Fingerprints.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/shared/Fingerprints.kt
@@ -4,6 +4,21 @@ import app.revanced.patcher.fingerprint
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
+internal val conversionContextFingerprintToString = fingerprint {
+ parameters()
+ strings(
+ "ConversionContext{containerInternal=",
+ ", widthConstraint=",
+ ", heightConstraint=",
+ ", templateLoggerFactory=",
+ ", rootDisposableContainer=",
+ ", identifierProperty="
+ )
+ custom { method, _ ->
+ method.name == "toString"
+ }
+}
+
internal val autoRepeatFingerprint = fingerprint {
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
returns("V")
diff --git a/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt b/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt
index c6de63c83..d65a95eaa 100644
--- a/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt
+++ b/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt
@@ -11,12 +11,14 @@ import app.revanced.patcher.patch.BytecodePatchContext
import app.revanced.patcher.patch.PatchException
import app.revanced.patcher.util.proxy.mutableTypes.MutableClass
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
+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.util.InstructionUtils.Companion.branchOpcodes
import app.revanced.util.InstructionUtils.Companion.returnOpcodes
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.iface.Method
@@ -168,6 +170,15 @@ internal val Instruction.isBranchInstruction: Boolean
internal val Instruction.isReturnInstruction: Boolean
get() = this.opcode in returnOpcodes
+/**
+ * Adds public [AccessFlags] and removes private and protected flags (if present).
+ */
+internal fun Int.toPublicAccessFlags() : Int {
+ return this.or(AccessFlags.PUBLIC.value)
+ .and(AccessFlags.PROTECTED.value.inv())
+ .and(AccessFlags.PRIVATE.value.inv())
+}
+
/**
* Find the [MutableMethod] from a given [Method] in a [MutableClass].
*
@@ -207,6 +218,26 @@ fun MutableMethod.injectHideViewCall(
"invoke-static { v$viewRegister }, $classDescriptor->$targetMethod(Landroid/view/View;)V",
)
+
+/**
+ * Inserts instructions at a given index, using the existing control flow label at that index.
+ * Inserted instructions can have it's own control flow labels as well.
+ *
+ * Effectively this changes the code from:
+ * :label
+ * (original code)
+ *
+ * Into:
+ * :label
+ * (patch code)
+ * (original code)
+ */
+// TODO: delete this on next major version bump.
+fun MutableMethod.addInstructionsAtControlFlowLabel(
+ insertIndex: Int,
+ instructions: String
+) = addInstructionsAtControlFlowLabel(insertIndex, instructions, *arrayOf())
+
/**
* Inserts instructions at a given index, using the existing control flow label at that index.
* Inserted instructions can have it's own control flow labels as well.
@@ -223,13 +254,14 @@ fun MutableMethod.injectHideViewCall(
fun MutableMethod.addInstructionsAtControlFlowLabel(
insertIndex: Int,
instructions: String,
+ vararg externalLabels: ExternalLabel
) {
// Duplicate original instruction and add to +1 index.
addInstruction(insertIndex + 1, getInstruction(insertIndex))
// Add patch code at same index as duplicated instruction,
// so it uses the original instruction control flow label.
- addInstructionsWithLabels(insertIndex + 1, instructions)
+ addInstructionsWithLabels(insertIndex + 1, instructions, *externalLabels)
// Remove original non duplicated instruction.
removeInstruction(insertIndex)
@@ -472,7 +504,7 @@ fun Method.indexOfFirstInstruction(startIndex: Int = 0, targetOpcode: Opcode): I
* @see indexOfFirstInstructionOrThrow
*/
fun Method.indexOfFirstInstruction(startIndex: Int = 0, filter: Instruction.() -> Boolean): Int {
- var instructions = this.implementation!!.instructions
+ var instructions = this.implementation?.instructions ?: return -1
if (startIndex != 0) {
instructions = instructions.drop(startIndex)
}
@@ -538,7 +570,7 @@ fun Method.indexOfFirstInstructionReversed(startIndex: Int? = null, targetOpcode
* @see indexOfFirstInstructionReversedOrThrow
*/
fun Method.indexOfFirstInstructionReversed(startIndex: Int? = null, filter: Instruction.() -> Boolean): Int {
- var instructions = this.implementation!!.instructions
+ var instructions = this.implementation?.instructions ?: return -1
if (startIndex != null) {
instructions = instructions.take(startIndex + 1)
}
diff --git a/patches/src/main/resources/addresources/values-fi-rFI/strings.xml b/patches/src/main/resources/addresources/values-fi-rFI/strings.xml
index 8b4242ce4..74f193c4e 100644
--- a/patches/src/main/resources/addresources/values-fi-rFI/strings.xml
+++ b/patches/src/main/resources/addresources/values-fi-rFI/strings.xml
@@ -460,15 +460,29 @@ Säädä äänenvoimakkuutta pyyhkäisemällä pystysuoraan näytön oikealta pu
Automaattinen kirkkaus otetaan käyttöön pyyhkäisemällä alhaisimpaan arvoon
Pienimpään arvoon alas pyyhkäiseminen ei ota käyttöön automaattista kirkkautta
Automaattinen
- Pyyhkäisyikkunan aikakatkaisu
+ Pyyhkäisypeittokuvan aikakatkaisu
Kuinka monta millisekuntia ikkuna on näkyvissä
Pyyhkäisypeittokuvan taustan läpinäkymättömyys
Läpinäkymättömyysarvo 0–100 välillä
- Pyyhkäisyn läpinäkymättömyyden on oltava välillä 0–100
+ Pyyhkäisypeittokuvan läpinäkymättömyyden tulee olla 0–100 välillä
+ Pyyhkäisypeittokuvan edistymispalkin väri
+ Äänenvoimakkuuden ja kirkkauden säätimien edistymispalkin väri
+ Virheellinen edistymispalkin väri
+ Pyyhkäisypeittokuvan tekstin koko
+ Pyyhkäisypeittokuvan tekstin koko 1–30 välillä
+ Tekstin koon tulee olla 1–30 välillä
Pyyhkäisyn kynnysraja
Pyyhkäisyä varten tarvittavan kynnyksen määrä
Äänenvoimakkuuden pyyhkäisyn herkkyys
Kuinka paljon äänenvoimakkuus muuttuu pyyhkäisyä kohden
+ Pyyhkäisypeittokuvan tyyli
+ Vaakasuuntainen peittokuva
+ Vaakasuuntainen peittokuva (minimaalinen – ylhäällä)
+ Vaakasuuntainen peittokuva (minimaalinen – keskellä)
+ Pyöreä peittokuva
+ Pyöreä peittokuva (minimaalinen)
+ Pystysuuntainen peittokuva
+ Pystysuuntainen peittokuva (minimaalinen)
Ota videon vaihto pyyhkäisemällä käyttöön
Pyyhkäisemällä kokoruututilassa siirrytään seuraavaan/edelliseen videoon
Pyyhkäisemällä kokoruututilassa ei siirrytä seuraavaan/edelliseen videoon
@@ -802,7 +816,7 @@ Asetukset → Toisto → Toista seuraava video automaattisesti"
Soittimen peittokuvan läpinäkymättömyys
Läpinäkymättömyysarvo välillä 0–100, jossa 0 on läpinäkyvä
- Soittimen peittokuvan läpinäkymättömyyden on oltava välillä 0–100
+ Soittimen peittokuvan läpinäkymättömyyden tulee olla 0–100 välillä
@@ -903,7 +917,7 @@ Tämä ominaisuus toimii parhaiten, kun videon laatu on 720p tai alhaisempi ja k
Luo uusi osio -painiketta ei näytetä
Uuden osion ajoituksen säätö
Kuinka monta millisekuntia ajansäätöpainikkeet liikkuvat uusia osioita luotaessa
- Arvon on oltava positiivinen luku
+ Arvon tulee olla positiivinen luku
Näytä ohjeet
Ohjeet sisältävät sääntöjä ja vinkkejä uusien osioiden luomiseen
Noudata ohjeita
@@ -1201,10 +1215,10 @@ Pyyhkäise laajentaaksesi tai sulkeaksesi"
Eteenpäin ja taaksepäin näytetään
Aloituskoko
Alkuperäinen näyttökoko pikseleinä
- Pikselikoon on oltava välillä %1$s ja %2$s
+ Pikselikoon tulee olla %1$s ja %2$s välillä
Peittokuvan läpinäkymättömyys
Läpinäkymättömyysarvo välillä 0–100, jossa 0 on läpinäkyvä
- Minisoittimen peittokuvan läpinäkymättömyyden on oltava välillä 0–100
+ Minisoittimen peittokuvan läpinäkymättömyyden tulee olla 0–100 välillä
Ota liukuvärillinen latausruutu käyttöön
@@ -1295,6 +1309,7 @@ Tämä voi avata korkealaatuisemmat videot"
GmsCoren asetukset
+ Jos olet äskettäin muuttanut tilisi kirjautumistietoja, poista ja asenna MicroG uudelleen.
Ohita URL-osoitteen uudelleenohjaukset
@@ -1352,7 +1367,7 @@ Tämä voi avata korkealaatuisemmat videot"
Omaa nopeusvalikkoa ei näytetä
Omat toistonopeudet
Lisää tai muuta omia toistonopeuksia
- Omien nopeuksien on oltava alle %s
+ Omien nopeuksien tulee olla alle %s
Virheelliset omat toistonopeudet
Automaattinen
Oma napauta ja pidä pohjassa -nopeus
diff --git a/patches/src/main/resources/addresources/values-ga-rIE/strings.xml b/patches/src/main/resources/addresources/values-ga-rIE/strings.xml
index 3ea410f96..5a2f05701 100644
--- a/patches/src/main/resources/addresources/values-ga-rIE/strings.xml
+++ b/patches/src/main/resources/addresources/values-ga-rIE/strings.xml
@@ -114,9 +114,9 @@ Brúigh an cnaipe leanúnaí agus ligean athruithe optúimíochta."
Taispeáin tósta ar earráid ReVanced
Taispeántar toast má tharlaíonn earráid
Ní thaispeántar toast má tharlaíonn earráid
- "Díchumasaíonn an rogha toasts earráide fógraí earráide ReVanced go léir.
+ "Má mhúchtar tóstaí earráide, folaítear gach fógra earráide ReVanced.
-Ní bheidh a fhios agat faoi aon imeachtaí neamhghnácha."
+Ní chuirfear ar an eolas thú faoi aon imeachtaí gan choinne."
Folaigh cártaí albam
@@ -205,11 +205,11 @@ Ní bheidh a fhios agat faoi aon imeachtaí neamhghnácha."
Tá painéil leighis i bhfolach
Taispeántar painéil leighis
Folaigh barra cainéal
- Tá barra cainéal i bhfolach
- Taispeántar barra cainéal
- Folaigh Rudaí Inimeartha
- Tá rudaí inimeartha i bhfolach
- Taispeántar rudaí inimeartha
+ Tá an barra Cainéal i bhfolach
+ Taispeántar barra an chainéil
+ Folaigh na heilimintí inseinnte
+ Tá na heilimintí inseinnte i bhfolach
+ Taispeántar na heilimintí inseinnte
Folaigh gníomhartha gasta i lánscáileán
Tá gníomhartha gasta i bhfolach
Taispeántar gníomhartha tapa
diff --git a/patches/src/main/resources/addresources/values-ja-rJP/strings.xml b/patches/src/main/resources/addresources/values-ja-rJP/strings.xml
index 88b96c63e..2ab5925f6 100644
--- a/patches/src/main/resources/addresources/values-ja-rJP/strings.xml
+++ b/patches/src/main/resources/addresources/values-ja-rJP/strings.xml
@@ -64,7 +64,7 @@ Second \"item\" text"
MicroG GmsCore がインストールされていません。インストールしてください。
必ず実行してください
- "MicroG GmsCore はバックグラウンドで実行するための権限を持っていません。
+ "MicroG GmsCore はバックグラウンドで動くための権限を持っていません。
下記ウェブサイト「Don't kill my app」の携帯電話メーカー別のガイドに従い、MicroG GmsCore に対するデバイスの設定を変更してください。
@@ -80,7 +80,7 @@ MicroG GmsCore に対する電池の最適化を無効にしても、バッテ
- このアプリについて
+ ReVanced について
広告
代替サムネイル
フィード
@@ -333,9 +333,9 @@ MicroG GmsCore に対する電池の最適化を無効にしても、バッテ
"ホーム / 登録チャンネル / 検索結果からキーワードに合致する動画を除外します
制限事項
-• ショート動画はチャンネル名で除外できません
-• 一部の UI コンポーネントが残ってしまう場合があります
-• キーワードを検索しても、結果が表示されない場合があります"
+• ショート動画はチャンネル名で除外されない
+• 一部の UI コンポーネントが残ってしまう場合がある
+• キーワードを検索したとき、結果が表示されない場合がある"
単語全体で合致させる
キーワードを二重引用符で囲むことで、動画のタイトルやチャンネル名の単語の一部とキーワードが合致しないようにできます<br><br>例えば、<br><b>\"ai\"</b>は、次の動画を除外します:<b>How does AI work?</b><br>しかし、次の動画は除外しません:<b>What does fair use mean?</b>
@@ -854,7 +854,7 @@ MicroG GmsCore に対する電池の最適化を無効にしても、バッテ
API 利用不可時にトーストを表示
Return YouTube Dislike が利用できない場合、トースト ポップアップが表示されます
Return YouTube Dislike が利用できない場合でもトースト ポップアップは表示されません
- このアプリについて
+ Return YouTube Dislike について
このデータはReturn YouTube Dislike APIによって提供されています。詳細はここをタップしてください
このデバイスでのReturnYouTubeDislike API 統計情報
@@ -1087,7 +1087,7 @@ MicroG GmsCore に対する電池の最適化を無効にしても、バッテ
色の値が無効です
色をリセット
リセット
- このアプリについて
+ SponsorBlock について
SponsorBlock APIによって提供されるデータです。詳細はこちらをタップしてください。
@@ -1099,11 +1099,11 @@ MicroG GmsCore に対する電池の最適化を無効にしても、バッテ
"変更点は以下のとおりです:
タブレット レイアウト
-• コミュニティ投稿が表示されません
+• コミュニティ投稿が表示されない
Automotive レイアウト
-• ショート動画を通常のプレーヤーで開きます
-• フィードがトピックとチャンネルで分類されます"
+• ショート動画を通常のプレーヤーで開く
+• フィードがトピックとチャンネルで分類される"
アプリのバージョンを偽装する
@@ -1189,8 +1189,8 @@ Automotive レイアウト
「ダブルタップとピンチでサイズ変更」を有効にする
"「ダブルタップとピンチでサイズ変更」は有効です
-• ダブルタップでミニプレーヤーのサイズを大きくします
-• もう一度ダブルタップすると、元のサイズに戻ります"
+• ダブルタップすると、ミニプレーヤーのサイズが大きくなる
+• もう一度ダブルタップすると、元のサイズに戻る"
「 ダブルタップとピンチでサイズ変更」は無効です
ドラッグ&ドロップを有効にする
"ドラッグ&ドロップは有効です
@@ -1311,7 +1311,7 @@ Automotive レイアウト
GmsCore の設定
- 最近アカウントのログイン情報を変更した場合は、MicroGをアンインストールして再インストールしてください。
+ 最近アカウントのログイン情報を変更した場合は、MicroG をアンインストールして再インストールしてください。
URL リダイレクトを回避する
@@ -1399,13 +1399,13 @@ Automotive レイアウト
動画ストリームを偽装する
- 再生不能問題を回避するために、クライアントの動画ストリームを偽装します
+ 動画の再生に失敗しないために、クライアントの動画ストリームを偽装します
動画ストリームを偽装する
動画ストリームは偽装されます
"動画ストリームは偽装されません
-動画が再生されない可能性があります"
- この設定をオフにすると、動画が再生されなくなる可能性があります。
+動画の再生に失敗する可能性があります"
+ この設定をオフにすると、動画の再生に失敗するようになる可能性があります。
デフォルトのクライアント
iOS クライアントで AVC (H.264) を強制的に使用する
ビデオ コーデックは強制的に AVC (H.264) が使用されます
@@ -1414,15 +1414,15 @@ Automotive レイアウト
AVC は、最大解像度が 1080p であり、Opus オーディオ コーデックが利用できず、動画再生時の通信量が VP9 や AV1 より多くなります。"
iOS クライアントの副作用
- "• 映画や有料動画が再生されない可能性があります
-•「 一定音量」が利用できません
-• 動画が 1 秒早く終了します"
+ "• 映画や有料動画が再生されない可能性がある
+•「 一定音量」が利用できない
+• 動画が 1 秒早く終了する"
Android クライアントの副作用
- "• 「音声トラック」がフライアウト メニューに表示されません
-• 「一定音量」が利用できません
-• 「デフォルトの吹き替えを無効にする」が利用できません"
- • AV1 コーデックが利用できません
- • ログアウト時またはシークレット モード時に、子ども向け動画が再生されない可能性があります
+ "• 「音声トラック」がフライアウト メニューに表示されない
+• 「一定音量」が利用できない
+• 「デフォルトの吹き替えを無効にする」が利用できない"
+ • AV1 コーデックが利用できない
+ • ログアウト時またはシークレット モード時に、子ども向け動画が再生されない可能性がある
統計情報に表示する
現在のクライアントが統計情報に表示されます
現在のクライアントは統計情報に表示されません
diff --git a/patches/src/main/resources/addresources/values-ko-rKR/strings.xml b/patches/src/main/resources/addresources/values-ko-rKR/strings.xml
index 409cccbdd..9f732a008 100644
--- a/patches/src/main/resources/addresources/values-ko-rKR/strings.xml
+++ b/patches/src/main/resources/addresources/values-ko-rKR/strings.xml
@@ -1035,7 +1035,7 @@ MicroG 앱 배터리 최적화를 비활성화(제한 없음)하더라도, 배
%1$s ~ %2$s
구간 카테고리를 선택하세요
이 카테고리는 비활성화되어 있습니다. 제출하려면 설정에서 활성화해야 합니다.
- 새 SponsorBlock 구간
+ 새로운 SponsorBlock 구간
%s 을 구간의 시작 또는 끝으로 설정하시겠습니까?
시작
끝
diff --git a/patches/src/main/resources/addresources/values-sr-rCS/strings.xml b/patches/src/main/resources/addresources/values-sr-rCS/strings.xml
index f0fd569d9..002c37efd 100644
--- a/patches/src/main/resources/addresources/values-sr-rCS/strings.xml
+++ b/patches/src/main/resources/addresources/values-sr-rCS/strings.xml
@@ -712,8 +712,8 @@ Da biste prikazali meni „Audio snimak”, promenite opciju „Lažirani video
Dugmad u plejeru pri pauzi su skrivena
Dugmad u plejeru pri pauzi su prikazana
Sakrij dugme „Prodavnica”
- Dugme „Kupovina” je skriveno
- Dugme „Kupovina” je prikazano
+ Dugme „Prodavnica” je skriveno
+ Dugme „Prodavnica” je prikazano
Sakrij dugme za kupovinu „Superhvala”
Dugme „Superhvala” je skriveno
Dugme „Superhvala” je prikazano
@@ -733,8 +733,8 @@ Da biste prikazali meni „Audio snimak”, promenite opciju „Lažirani video
Dugme „Predstojeće” je skriveno
Dugme „Predstojeće” je prikazano
Sakrij dugme „Zeleni ekran”
- Dugme „Green Screen” je skriveno
- Dugme „Green Screen” je prikazano
+ Dugme „Zeleni ekran” je skriveno
+ Dugme „Zeleni ekran” je prikazano
Sakrij dugme heš-oznake
Dugme heš-oznake je skriveno
Dugme heš-oznake je prikazano
diff --git a/patches/src/main/resources/addresources/values-sr-rSP/strings.xml b/patches/src/main/resources/addresources/values-sr-rSP/strings.xml
index 8329c5c01..37a41fa5a 100644
--- a/patches/src/main/resources/addresources/values-sr-rSP/strings.xml
+++ b/patches/src/main/resources/addresources/values-sr-rSP/strings.xml
@@ -712,8 +712,8 @@ Second \"item\" text"
Дугмад у плејеру при паузи су скривена
Дугмад у плејеру при паузи су приказана
Сакриј дугме „Продавница”
- Дугме „Куповина” је скривено
- Дугме „Куповина” је приказано
+ Дугме „Продавница” је скривено
+ Дугме „Продавница” је приказано
Сакриј дугме за куповину „Суперхвала”
Дугме „Суперхвала” је скривено
Дугме „Суперхвала” је приказано
@@ -733,8 +733,8 @@ Second \"item\" text"
Дугме „Предстојеће” је скривено
Дугме „Предстојеће” је приказано
Сакриј дугме „Зелени екран”
- Дугме „Green Screen” је скривено
- Дугме „Green Screen” је приказано
+ Дугме „Зелени екран” је скривено
+ Дугме „Зелени екран” је приказано
Сакриј дугме хеш-ознаке
Дугме хеш-ознаке је скривено
Дугме хеш-ознаке је приказано
diff --git a/patches/src/main/resources/addresources/values-uk-rUA/strings.xml b/patches/src/main/resources/addresources/values-uk-rUA/strings.xml
index 98eb7eabd..bbb2a659f 100644
--- a/patches/src/main/resources/addresources/values-uk-rUA/strings.xml
+++ b/patches/src/main/resources/addresources/values-uk-rUA/strings.xml
@@ -495,12 +495,12 @@ Second \"item\" text"
Кнопки дій
Приховати або показувати кнопки дій під відео
- Вимкнути відблиск кнопок \"Подобається\" та \"Підписати\"
- Кнопки \"Подобається\" та \"Підписатися\" не відблискуватимуть при згадуванні
- Кнопки \"Подобається\" та \"Підписатися\" не відблискуватимуть при згадуванні
- Приховати \"Подобається\" та \"Не подобається\"
- Кнопки \"Подобається\" та \"Не подобається\" приховано
- Кнопки \"Подобається\" та \"Не подобається\" показуються
+ Вимкнути блимання кнопок \"Лайк\" та \"Підписатися\"
+ Кнопки \"Лайк\" та \"Підписатися\" не будуть блимати при згадуванні
+ Кнопки \"Лайк\" та \"Підписатися\" не будуть блимати при згадуванні
+ Приховати \"Лайк\" та \"Дизлайк\"
+ Кнопки \"Лайк\" та \"Дизлайк\" приховано
+ Кнопки \"Лайк\" та \"Дизлайк\" показуються
Приховати \"Поділитися\"
Кнопку \"Поділитися\" приховано
@@ -745,14 +745,14 @@ Second \"item\" text"
Стікери приховано
Стікери показуються
Приховати анімацію фонтану
- Анімацію фонтану біля кнопки \"Подобається\" приховано
- Анімація фонтану біля кнопки \"Подобається\" показується
- Приховати \"Подобається\"
- Кнопку \"Подобається\" приховано
- Кнопка \"Подобається\" показується
- Приховати \"Не подобається\"
- Кнопку \"Не подобається\" приховано
- Кнопка \"Не подобається\" показується
+ Анімацію фонтану біля кнопки \"Лайк\" приховано
+ Анімація фонтану біля кнопки \"Лайк\" показується
+ Приховати \"Лайк\"
+ Кнопку \"Лайк\" приховано
+ Кнопка \"Лайк\" показується
+ Приховати \"Дизлайк\"
+ Кнопку \"Дизлайк\" приховано
+ Кнопка \"Дизлайк\" показується
Приховати \"Коментарі\"
Кнопку \"Коментарі\" приховано
Кнопка \"Коментарі\" показується
@@ -823,43 +823,43 @@ Second \"item\" text"
- Відмітки \"Не подобається\" тимчасово недоступні (тайм-аут API)
- Відмітки \"Не подобається\" недоступні (статус %d)
- Відмітки \"Не подобається\" недоступні (ліміт клієнтів API)
- Відмітки \"Не подобається\" недоступні (%s)
+ Дизлайки тимчасово недоступні (тайм-аут API)
+ Дизлайки недоступні (статус %d)
+ Дизлайки недоступні (ліміт клієнтів API)
+ Дизлайки недоступні (%s)
- Оновіть відео, щоб проголосувати за допомогою ReturnYouTubeDislike
+ Оновіть відео, щоб проголосувати за допомогою Return YouTube Dislike
Приховано власником
- Відмітки \"Не подобається\" показуються
- Відмітки \"Не подобається\" не показуються
- Відмітки \"Не подобається\" в Shorts
- "Відмітки \"Не подобається\" в Shorts показуються
+ Дизлайки показуються
+ Дизлайки не показуються
+ Показувати дизлайки в Shorts
+ "Дизлайки в Shorts показуються
-Обмеження: Відмітки \"Не подобається\" не можуть показуватися в анонімному режимі"
- Відмітки \"Не подобається\" в Shorts не показуються
- Відмітки \"Не подобається\" у відсотках
- Відмітки \"Не подобається\" показуються у відсотках
- Відмітки \"Не подобається\" показуються як число
+Обмеження: Дизлайки не можуть показуватися в анонімному режимі"
+ Дизлайки в Shorts не показуються
+ Дизлайки у відсотках
+ Дизлайки показуються у відсотках
+ Дизлайки показуються як число
- Компактна кнопка \"Подобається\"
- Кнопку \"Подобається\" стилізовано під мінімальну ширину
- Кнопку \"Подобається\" стилізовано для кращого вигляду
- Показувати приблизну кількість вподобань
- На відео з вимкненими відмітками \"Подобається\" показується приблизна кількість вподобань
- Приблизна кількість вподобань не показується
+ Компактна кнопка \"Лайк\"
+ Кнопку \"Лайк\" стилізовано під мінімальну ширину
+ Кнопку \"Лайк\" стилізовано для кращого вигляду
+ Показувати приблизну кількість лайків
+ Для відео з вимкненими лайками показується приблизна кількість лайків
+ Приблизна кількість лайків не показується
Показувати тост, якщо API не доступний
Тост показується, якщо Return YouTube Dislike не доступний
Тост не показується, якщо Return YouTube Dislike не доступний
Про інтеграцію
- Дані надаються Return YouTube Dislike API. Натисніть тут, щоб дізнатися більше
+ Дані надаються через API Return YouTube Dislike. Натисніть тут, щоб дізнатися більше
- Статистика ReturnYouTubeDislike API цього пристрою
+ Статистика API ReturnYouTubeDislike цього пристрою
Час відповіді API, середній
Час відповіді API, мінімальний
Час відповіді API, максимальний
Час відповіді API, останнє відео
- Відмітки \"Не подобається\" тимчасово недоступні - діє обмеження швидкості клієнта API
+ Дизлайки тимчасово недоступні - діє обмеження швидкості для клієнта API
Отримання голосів API, кількість запитів
Мережевих запитів не здійснено
Здійснено %d мережевих запитів
@@ -948,13 +948,13 @@ Second \"item\" text"
Імпорт / Експорт налаштувань
Копіювати
Ваші налаштування SponsorBlock, які можуть бути імпортовані чи експортовані у ReVanced та до інших платформ SponsorBlock
- Ваші налаштування SponsorBlock, які можуть бути імпортовані чи експортовані у ReVanced та до інших платформ SponsorBlock. Вони також містять Ваш особистий ідентифікатор користувача. Діліться ним розумно
+ Ваші налаштування SponsorBlock, які можуть бути імпортовані чи експортовані у ReVanced та до інших платформ SponsorBlock. Вони також містять Ваш особистий ідентифікатор користувача. Діліться ними розумно
Налаштування успішно імпортовано
Не вдалося імпортувати: %s
Не вдалося експортувати: %s
- "Ваші налаштування містять особистий ID користувача SponsorBlock.
+ "Ваші налаштування містять особистий ідентифікатор користувача SponsorBlock.
-Ваш ID користувача це як пароль і його не можна поширювати."
+Ваш ідентифікатор користувача це як пароль і його не можна поширювати."
Більше не показувати
Змінити поведінку сегмента
Спонсор
@@ -1020,10 +1020,10 @@ Second \"item\" text"
Він вже існує"
Сегмент успішно надіслано
- SponsorBlock тимчасово недоступний (закінчився час API)
+ SponsorBlock тимчасово недоступний (тайм-аут API)
SponsorBlock тимчасово недоступний (статус %d)
SponsorBlock тимчасово не доступний
- Не вдалося проголосувати за сегмент (закінчився час API)
+ Не вдалося проголосувати за сегмент (тайм-аут API)
Не вдалося проголосувати за сегмент (статус: %1$d %2$s)
Не вдалося проголосувати за сегмент: %s
Проголосувати \"за\"
@@ -1087,7 +1087,7 @@ Second \"item\" text"
Скинути колір
Скинути
Про інтеграцію
- Дані надаються SponsorBlock API. Натисніть тут, щоб дізнатися більше та побачити завантаження для інших платформ
+ Дані надаються через API SponsorBlock. Натисніть тут, щоб дізнатися більше та побачити завантаження для інших платформ
Макет інтерфейсу
@@ -1311,7 +1311,7 @@ Second \"item\" text"
Відкрити GmsCore для налаштування та входу в обліковий запис Google
- Якщо ви нещодавно змінили дані для входу у свій обліковий запис, видаліть і повторно встановіть MicroG.
+ Якщо Ви нещодавно змінили дані для входу у свій обліковий запис, видаліть і повторно встановіть MicroG.
Обхід URL переадресацій
diff --git a/patches/src/main/resources/addresources/values-vi-rVN/strings.xml b/patches/src/main/resources/addresources/values-vi-rVN/strings.xml
index adad5e60d..549c22816 100644
--- a/patches/src/main/resources/addresources/values-vi-rVN/strings.xml
+++ b/patches/src/main/resources/addresources/values-vi-rVN/strings.xml
@@ -24,7 +24,7 @@ Second \"item\" text"
Kiểm tra thất bại
Mở trang web chính thức
Bỏ qua
- <h5>Ứng dụng này xem ra không phải do bạn tự vá.</h5><br>Ứng dụng này có thể không hoạt động chính xác, <b>tiềm ẩn rủi ro hoặc thậm chí gây nguy hiểm khi sử dụng</b>.<br><br>Những kiểm tra này ngụ ý rằng ứng dụng được vá sẵn hoặc lấy từ nguồn khác;<br><br><small>%1$s</small><br>Chúng tôi khuyến nghị bạn nên <b>gỡ cài đặt ứng này và tự vá lại</b> để đảm bảo bạn đang dùng một ứng dụng an toàn và hợp lệ.<p><br>Cảnh báo này sẽ chỉ hiện hai lần, hãy cân nhắc trước khi bỏ qua.
+ <h5>Ứng dụng này xem ra không phải do bạn tự vá.</h5><br>Ứng dụng này có thể không hoạt động chính xác, <b>tiềm ẩn rủi ro hoặc thậm chí gây nguy hiểm khi sử dụng</b>.<br><br>Những kiểm tra dưới đây cho thấy rằng ứng dụng được vá sẵn hoặc lấy từ nguồn khác;<br><br><small>%1$s</small><br>Chúng tôi khuyến nghị bạn nên <b>gỡ cài đặt ứng này và tự vá lại</b> để đảm bảo bạn đang dùng một ứng dụng an toàn và hợp lệ.<p><br>Cảnh báo này sẽ chỉ hiện hai lần, hãy cân nhắc trước khi bỏ qua.
Đã vá trên một thiết bị khác
Không được cài đặt bởi ReVanced Manager
Đã vá hơn 10 phút trước
@@ -315,7 +315,7 @@ Nếu cài đặt này được bật và Doodle đang hiển thị tại khu v
Ẩn kết quả tìm kiếm bằng từ khóa
Kết quả tìm kiếm đã được lọc bằng từ khóa
Kết quả tìm kiếm không được lọc bằng từ khóa
- Ẩn video đăng ký bằng từ khóa
+ Ẩn video kênh đăng ký bằng từ khóa
Video ở thẻ đăng ký đã được lọc bằng từ khóa
Video ở thẻ đăng ký không được lọc bằng từ khóa
Từ khóa để ẩn
@@ -399,7 +399,7 @@ Tính năng này chỉ khả dụng cho các thiết bị cũ hơn"
Đã chép URL vào bảng nhớ tạm
Đã chép URL với dấu thời gian
- Hiện nút sao chép url video
+ Hiện nút sao chép URL video
Nút được hiển thị. Chạm để sao chép video URL. Chạm và giữ để sao chép với dấu thời gian
Nút không được hiển thị
Hiện nút sao chép URL với dấu thời gian
@@ -416,7 +416,7 @@ Tính năng này chỉ khả dụng cho các thiết bị cũ hơn"
Tải xuống bên ngoài
Các thiết lập trình tải xuống bên ngoài
Hiện nút tải xuống bên ngoài
- Nút tải xuống trong trình phát được hiển thị
+ Nút tải xuống trong trình phát đã được hiển thị
Nút tải xuống trong trình phát không được hiển thị
Thay thế nút hành động Tải xuống
@@ -446,12 +446,12 @@ Tính năng này chỉ khả dụng cho các thiết bị cũ hơn"
"Đã bật vuốt âm lượng toàn màn hình
Điều chỉnh âm lượng bằng cách vuốt dọc ở bên phải màn hình"
- Vuốt âm lượng được tắt
- Bật cử chỉ nhấn-để-vuốt
- Nhấn-để-vuốt đã bật
- Nhấn-để-vuốt đã tắt
+ Vuốt âm lượng toàn màn hình đã tắt
+ Bật cử chỉ nhấn giữ để vuốt
+ Nhấn giữ để vuốt đã bật
+ Nhấn giữ để vuốt đã tắt
Bật phản hồi xúc giác
- Phản hồi xúc giác đã bật
+ Phản hồi xúc giác đã được bật
Phản hồi xúc giác đã tắt
Lưu và khôi phục độ sáng
Lưu và khôi phục độ sáng khi thoát hoặc vào toàn màn hình
@@ -496,8 +496,8 @@ Tính năng này chỉ khả dụng cho các thiết bị cũ hơn"
Các nút hành động
Ẩn hoặc hiện nút dưới video
Tắt hiệu ứng phát sáng nút Thích và Đăng ký
- Nút Thích và Đăng ký sẽ không phát sáng khi được đề cập đến
- Nút Thích và Đăng ký sẽ phát sáng khi được đề cập đến
+ Nút Thích và Đăng ký sẽ không phát sáng khi được tương tác
+ Nút Thích và Đăng ký sẽ phát sáng khi được tương tác
Ẩn Thích và Không thích
Các nút Thích và Không thích đã bị ẩn
Các nút Thích và Không thích được hiển thị
@@ -525,8 +525,8 @@ Tính năng này chỉ khả dụng cho các thiết bị cũ hơn"
Ẩn Hỏi
- Nút Hỏi đã bị ẩn
- Nút Hỏi được hiển thị
+ Nút hỏi đã bị ẩn
+ Nút hỏi được hiển thị
Ẩn Tạo đoạn video
Nút tạo đoạn video đã bị ẩn
@@ -538,26 +538,26 @@ Tính năng này chỉ khả dụng cho các thiết bị cũ hơn"
Các nút điều hướng
- Ẩn hoặc hiện các nút ở thanh điều hướng
+ Ẩn hoặc thay đổi các nút ở thanh điều hướng
- Ẩn Trang chính
- Nút trang chính đã bị ẩn
- Nút trang chính được hiển thị
+ Ẩn Trang chủ
+ Nút trang chủ đã bị ẩn
+ Nút trang chủ được hiển thị
Ẩn Shorts
Nút Shorts đã bị ẩn
Nút Shorts được hiển thị
- Ẩn Tạo mới
+ Ẩn Tạo
Nút tạo đã bị ẩn
Nút tạo được hiển thị
- Ẩn Đăng ký
- Nút đăng ký đã bị ẩn
- Nút Đăng ký được hiển thị
+ Ẩn Kênh đăng ký
+ Nút kênh đăng ký đã bị ẩn
+ Nút kênh đăng ký được hiển thị
Ẩn Thông báo
- Nút Thông báo đã bị ẩn
- Nút Thông báo được hiển thị
+ Nút thông báo đã bị ẩn
+ Nút thông báo được hiển thị
Chuyển vị nút Tạo với nút Thông báo
"Nút tạo được chuyển đổi với nút Thông báo
@@ -577,7 +577,7 @@ Nếu việc thay đổi cài đặt này không có hiệu lực, hãy thử ch
Vô hiệu hóa thanh điều hướng trong suốt ở chế độ sáng
Thanh điều hướng ở chế độ sáng không trong suốt
Thanh điều hướng ở chế độ sáng là đục hoặc trong mờ
- Vô hiệu hoá thanh điều hướng trong mờ tối
+ Vô hiệu hoá thanh điều hướng trong chế độ tối
Thanh điều hướng ở chế độ tối không trong suốt
Thanh điều hướng ở chế độ tối là đục hoặc trong mờ
@@ -601,7 +601,7 @@ Nếu việc thay đổi cài đặt này không có hiệu lực, hãy thử ch
Trình đơn lặp video đã bị ẩn
Trình đơn lặp video được hiển thị
- Ẩn chế độ môi trường
+ Ẩn Chế độ môi trường
Trình đơn chế độ môi trường đã bị ẩn
Trình đơn chế độ môi trường được hiển thị
Ẩn Âm lượng ổn định
@@ -661,8 +661,8 @@ Nếu việc thay đổi cài đặt này không có hiệu lực, hãy thử ch
Thẻ kết thúc màn hình được hiển thị
- Tắt chế độ môi trường khi toàn màn hình
- Chế độ môi trường được tắt
+ Tắt Chế độ môi trường khi toàn màn hình
+ Chế độ môi trường đã tắt
Chế độ môi trường được bật
@@ -687,12 +687,12 @@ Nếu việc thay đổi cài đặt này không có hiệu lực, hãy thử ch
Trình phát Shorts
Ẩn hoặc hiện các thành phần trong trình phát Shorts
- Ẩn Shorts trong bảng tin trang chính
- Ẩn trong nguồn cấp dữ liệu trang chủ và video liên quan
- Hiện trong nguồn cấp dữ liệu trang chủ và video liên quan
+ Ẩn Shorts trong thẻ trang chủ
+ Ẩn trong thẻ trang chủ và video liên quan
+ Hiện trong thẻ trang chủ và video liên quan
- Ẩn Shorts trong bảng tin đăng ký
- Bị ẩn trong nguồn đăng ký
+ Ẩn Shorts trong thẻ kênh đăng ký
+ Bị ẩn trong thẻ kênh đăng ký
Được hiện trong nguồn đăng ký
Ẩn Shorts trong kết quả tìm kiếm
Bị ẩn trong kết quả tìm kiếm
@@ -806,10 +806,10 @@ Cài đặt → Phát → Tự động phát video tiếp theo"
Thoát chế độ toàn màn hình khi kết thúc video
- Đã tắt
- Chân dung
- Phong cảnh
- Chân dung và phong cảnh
+ Tắt
+ Chế độ dọc
+ Chế độ ngang
+ Chế độ dọc và ngang
Mở video ở chế độ toàn màn hình dọc
@@ -916,10 +916,10 @@ Tính năng này hoạt động tốt nhất với chất lượng video 720p tr
Thời lượng đầy đủ của video được hiện
Tạo các phân đoạn mới
Hiện nút Tạo phân đoạn mới
- Nút tạo phân đoạn mới được hiển thị
+ Nút tạo phân đoạn mới đã được hiển thị
Nút tạo phân đoạn mới không được hiển thị
Điều chỉnh bước tua của phân đoạn mới
- Số mili-giây của các nút điều chỉnh thay đổi khi tạo phân đoạn mới
+ Số mili-giây mà các nút điều chỉnh thời gian sẽ tua khi tạo phân đoạn mới
Giá trị phải là một số dương
Xem hướng dẫn
Hướng dẫn bao gồm các quy tắc và mẹo về cách tạo phân đoạn mới
@@ -1174,9 +1174,9 @@ Giới hạn: Sử dụng nút quay lại trên thanh công cụ có thể khôn
Trình phát thu nhỏ
Thay đổi kiểu trình phát thu nhỏ trong ứng dụng
Loại trình phát thu nhỏ
- Đã tắt
+ Tắt
Mặc định
- Thu gọn
+ Tối giản
Máy tính bảng
Hiện đại 1
Hiện đại 2
@@ -1185,22 +1185,22 @@ Giới hạn: Sử dụng nút quay lại trên thanh công cụ có thể khôn
Bật góc bo tròn
Góc được bo tròn
Góc vuông
- Bật nhấp đôi và chụm để thay đổi kích thước
- "Thao tác nhấn đúp và vuốt để thay đổi kích thước được bật
+ Bật nhấp đúp và chụm để thay đổi kích thước
+ "Thao tác nhấn đúp và chụm để thay đổi kích thước đã được bật
-• Nhấn đúp để tăng kích thước trình phát nhỏ
+• Nhấn đúp để tăng kích thước trình phát thu nhỏ
• Nhấn đúp lại để khôi phục kích thước ban đầu"
- Chạm đôi và chụm để thay đổi kích thước được tắt
+ Chạm đôi và chụm để thay đổi kích thước đã tắt
Bật kéo và thả
- "Kéo và thả được bật
+ "Kéo và thả đã được bật
-Trình phát nhỏ có thể được kéo đến bất kỳ góc nào của màn hình"
- Kéo và thả được tắt
+Trình phát thu nhỏ có thể được kéo đến bất kỳ góc nào của màn hình"
+ Kéo và thả đã tắt
Bật cử chỉ kéo ngang
- "Cử chỉ kéo ngang được bật
+ "Cử chỉ kéo ngang đã được bật
-Trình phát nhỏ có thể được kéo ra khỏi màn hình sang trái hoặc phải"
- Cử chỉ kéo ngang được tắt
+Trình phát thu nhỏ có thể được kéo ra mép màn hình sang bên trái hoặc phải"
+ Cử chỉ kéo ngang đã tắt
Ẩn các nút lớp phủ
Các nút lớp phủ đã bị ẩn
Các nút lớp phủ được hiển thị
@@ -1212,15 +1212,15 @@ Vuốt để mở rộng hoặc đóng"
Ẩn văn bản phụ
Văn bản phụ đã bị ẩn
Văn bản phụ được hiển thị
- Ẩn các nút bỏ quả đến tiếp và trước đó
- Các nút bỏ quả đến tiếp và trước đó đã bị ẩn
- Các nút bỏ quả đến tiếp và trước đó được hiển thị
+ Ẩn các nút tua nhanh và tua lại
+ Các nút tua nhanh và tua lại đã bị ẩn
+ Các nút tua nhanh và tua lại được hiển thị
Kích thước ban đầu
Kích thước ban đầu trên màn hình, bằng pixel
Pixel phải nằm giữa %1$s và %2$s
Độ mờ lớp phủ
Giá trị độ mờ của lớp phủ trình phát trong khoảng từ 0 đến 100, trong đó 0 là trong suốt
- Độ phủ mờ trình phát thu nhỏ phải nằm giữa 0-100
+ Độ mờ lớp phủ trình phát thu nhỏ phải nằm trong khoảng từ 0 đến 100
Bật màn hình tải màu dốc
@@ -1292,9 +1292,9 @@ Nhấn vào đây để tìm hiểu thêm về DeArrow"
Không hiện lại
- Bật tự phát lại
- Tự phát lại được bật
- Tự phát lại được tắt
+ Bật tự phát lặp lại
+ Tự phát lặp lại đã được bật
+ Tự phát lặp lại đã tắt
Giả mạo kích thước thiết bị
@@ -1325,7 +1325,7 @@ Bật tính năng này có thể mở khóa chất lượng video cao hơn"
Loại bỏ tham số truy vấn theo dõi
- Tham số truy vấn theo dõi được loại bỏ khỏi liên kết
+ Tham số truy vấn theo dõi đã bị loại bỏ khỏi liên kết
Tham số truy vấn theo dõi không được loại bỏ khỏi liên kết
@@ -1373,7 +1373,7 @@ Bật tính năng này có thể mở khóa chất lượng video cao hơn"Tốc độ phát lại tùy chỉnh không hợp lệ
Tự động
Tốc độ chạm và giữ tùy chỉnh
- Tốc độ phát lại giữa 0-8
+ Tốc độ phát từ 0 đến 8
Nhớ các thay đổi tốc độ phát