From efef03b80da21552d0d8be6913faba64e4fb5ed1 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Fri, 2 May 2025 14:58:25 +0200 Subject: [PATCH 01/18] feat(Lightroom): Constrain patches to last working version --- .../patches/lightroom/misc/login/DisableMandatoryLoginPatch.kt | 2 +- .../patches/lightroom/misc/premium/UnlockPremiumPatch.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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. From 7a245d37485b0b11b364a01d6ebfc5e7cbe7b34e Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Fri, 2 May 2025 13:02:25 +0000 Subject: [PATCH 02/18] chore: Release v5.23.0-dev.1 [skip ci] # [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)) --- CHANGELOG.md | 7 +++++++ gradle.properties | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ccdad47e6..1f80c04a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [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/gradle.properties b/gradle.properties index ce1b08bf3..645f8d101 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.1 From 7b43986871a68e5cb43331d2fb2fdb9ef67438ad Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Sun, 4 May 2025 13:55:12 +0400 Subject: [PATCH 03/18] fix(YouTube): Improve litho filtering performance (#4904) --- patches/api/patches.api | 1 + .../youtube/misc/litho/filter/Fingerprints.kt | 4 + .../misc/litho/filter/LithoFilterPatch.kt | 198 +++++++++++------- .../kotlin/app/revanced/util/BytecodeUtils.kt | 24 ++- 4 files changed, 150 insertions(+), 77 deletions(-) diff --git a/patches/api/patches.api b/patches/api/patches.api index 7d32bec85..1efb1246b 100644 --- a/patches/api/patches.api +++ b/patches/api/patches.api @@ -1534,6 +1534,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/youtube/misc/litho/filter/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/litho/filter/Fingerprints.kt index 89db01f0e..21a87879b 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 @@ -45,6 +45,10 @@ internal val readComponentIdentifierFingerprint = fingerprint { strings("Number of bits must be positive") } +internal val elementConfigFingerprint = fingerprint { + strings(" enableDroppedFrameLogging", " elementDepthInTree") +} + 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..7fb171646 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,27 +4,33 @@ 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.PatchException import app.revanced.patcher.patch.bytecodePatch -import app.revanced.patcher.util.smali.ExternalLabel +import app.revanced.patcher.util.proxy.mutableTypes.MutableField.Companion.toMutable +import app.revanced.patches.youtube.layout.returnyoutubedislike.conversionContextFingerprint 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.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.indexOfFirstInstructionReversed 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.FiveRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction -import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction import com.android.tools.smali.dexlib2.iface.reference.FieldReference import com.android.tools.smali.dexlib2.iface.reference.MethodReference +import com.android.tools.smali.dexlib2.immutable.ImmutableField lateinit var addLithoFilter: (String) -> Unit private set @@ -53,42 +59,48 @@ 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.readComponentIdentifier(...) method. * public void someOtherMethod(ByteBuffer byteBuffer) { * ExtensionClass.setProtoBuffer(byteBuffer); // Inserted by this patch. * ... * } * } * - * When patching 19.17 and earlier: + * When patching 19.16: * * class ComponentContextParser { - * public ComponentContext ReadComponentIdentifierFingerprint(...) { + * public Component readComponentIdentifier(...) { * ... - * if (extensionClass.filter(identifier, pathBuilder)); // Inserted by this patch. + * if (extensionClass.filter(identifier, pathBuilder)) { // Inserted by this patch. * return emptyComponent; - * ... + * } + * return originalUnpatchedComponent; * } * } * * When patching 19.18 and later: * * class ComponentContextParser { - * public ComponentContext parseBytesToComponentContext(...) { + * public ComponentIdentifierObj readComponentIdentifier(...) { * ... - * if (ReadComponentIdentifierFingerprint() == null); // Inserted by this patch. - * return emptyComponent; + * if (extensionClass.filter(identifier, pathBuilder)) { // Inserted by this patch. + * this.patch_isFiltered = true; + * } * ... * } * - * public ComponentIdentifierObj readComponentIdentifier(...) { - * ... - * if (extensionClass.filter(identifier, pathBuilder)); // Inserted by this patch. - * return null; + * public Component parseBytesToComponentContext(...) { * ... + * if (this.patch_isFiltered) { // Inserted by this patch. + * return emptyComponent; + * } + * return originalUnpatchedComponent; * } * } */ @@ -115,14 +127,13 @@ 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) @@ -132,44 +143,47 @@ val lithoFilterPatch = bytecodePatch( builderMethodDescriptor.returnType == classDef.type }!!.immutableClass.fields.single() + // Add a field to store the result of the filtering. This allows checking the field + // just before returning so the original code always runs the same when filtering occurs. + val lithoFilterResultField = ImmutableField( + componentContextParserFingerprint.classDef.type, + "patch_isFiltered", + "Z", + AccessFlags.PRIVATE.value, + null, + null, + null, + ).toMutable() + componentContextParserFingerprint.classDef.fields.add(lithoFilterResultField) + // 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 - """ + fun returnEmptyComponentInstructions(free: Int): String = """ + move-object/from16 v$free, p0 + iget-boolean v$free, v$free, $lithoFilterResultField + if-eqz v$free, :unfiltered + + move-object/from16 v$free, p1 + invoke-static { v$free }, $builderMethodDescriptor + move-result-object v$free + iget-object v$free, v$free, $emptyComponentField + return-object v$free + + :unfiltered + nop + """ 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 + findInstructionIndicesReversedOrThrow(Opcode.RETURN_OBJECT).forEach { index -> + val free = findFreeRegister(index) + + addInstructionsAtControlFlowLabel( + index, + returnEmptyComponentInstructions(free) + ) } - - // 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)), - ) } } @@ -178,47 +192,79 @@ val lithoFilterPatch = bytecodePatch( // region Read component then store the result. readComponentIdentifierFingerprint.method.apply { - val insertHookIndex = indexOfFirstInstructionOrThrow { - opcode == Opcode.IPUT_OBJECT && - getReference()?.type == "Ljava/lang/StringBuilder;" + val returnIndex = indexOfFirstInstructionReversedOrThrow(Opcode.RETURN_OBJECT) + if (indexOfFirstInstructionReversed(returnIndex - 1, Opcode.RETURN_OBJECT) >= 0) { + throw PatchException("Found multiple return indexes") // Patch needs an update. + } + + val elementConfigClass = elementConfigFingerprint.originalClassDef + val elementConfigClassType = elementConfigClass.type + val elementConfigIndex = indexOfFirstInstructionReversedOrThrow(returnIndex) { + val reference = getReference() + reference?.definingClass == elementConfigClassType + } + val elementConfigStringBuilderField = elementConfigClass.fields.single { field -> + field.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) { + val putStringBuilderIndex = indexOfFirstInstructionOrThrow { + val reference = getReference() + opcode == Opcode.IPUT_OBJECT && + reference?.definingClass == elementConfigClassType && + reference.type == "Ljava/lang/StringBuilder;" + } + val elementConfigIdentifierField = getInstruction( + indexOfFirstInstructionReversedOrThrow(putStringBuilderIndex) { + val reference = getReference() opcode == Opcode.IPUT_OBJECT && - getReference()?.type == "Ljava/lang/String;" - }, - ).registerA + reference?.definingClass == elementConfigClassType && + reference.type == "Ljava/lang/String;" + } + ).getReference() + + // Could use some of these free registers multiple times, but this is inserting at a + // return instruction so there is always multiple 4-bit registers available. + val elementConfigRegister = getInstruction(elementConfigIndex).registerC + val identifierRegister = findFreeRegister(returnIndex, elementConfigRegister) + val stringBuilderRegister = findFreeRegister(returnIndex, elementConfigRegister, identifierRegister) + val thisRegister = findFreeRegister(returnIndex, elementConfigRegister, identifierRegister, stringBuilderRegister) + val freeRegister = findFreeRegister(returnIndex, elementConfigRegister, identifierRegister, stringBuilderRegister, thisRegister) - val freeRegister = findFreeRegister(insertHookIndex, identifierRegister, stringBuilderRegister) val invokeFilterInstructions = """ + iget-object v$identifierRegister, v$elementConfigRegister, $elementConfigIdentifierField + iget-object v$stringBuilderRegister, v$elementConfigRegister, $elementConfigStringBuilderField 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 + move-object/from16 v$thisRegister, p0 + iput-boolean v$freeRegister, v$thisRegister, $lithoFilterResultField """ - addInstructionsWithLabels( - insertHookIndex, - if (is_19_18_or_greater) { + if (is_19_18_or_greater) { + addInstructionsAtControlFlowLabel( + returnIndex, + invokeFilterInstructions + ) + } else { + val elementConfigMethod = conversionContextFingerprint.originalClassDef.methods + .single { method -> + !AccessFlags.STATIC.isSet(method.accessFlags) && method.returnType == elementConfigClassType + } + + addInstructionsAtControlFlowLabel( + returnIndex, """ - $invokeFilterInstructions + # Element config is a method on a parameter. + move-object/from16 v$elementConfigRegister, p2 + invoke-virtual { v$elementConfigRegister }, $elementConfigMethod + move-result-object v$elementConfigRegister - # Return null, and the ComponentContextParserFingerprint hook - # handles returning an empty component. - const/4 v$freeRegister, 0x0 - return-object v$freeRegister - """ - } else { - """ $invokeFilterInstructions - - ${createReturnEmptyComponentInstructions(freeRegister)} + + ${returnEmptyComponentInstructions(freeRegister)} """ - }, - ExternalLabel("unfiltered", getInstruction(insertHookIndex)), - ) + ) + } } // endregion diff --git a/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt b/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt index c6de63c83..7005ed47a 100644 --- a/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt +++ b/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt @@ -11,6 +11,7 @@ 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 @@ -207,6 +208,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 +244,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) From b71fd28483ad0b3ebe9aef0c0454cd32cf58abe2 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Sun, 4 May 2025 09:58:25 +0000 Subject: [PATCH 04/18] chore: Release v5.23.0-dev.2 [skip ci] # [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)) --- CHANGELOG.md | 7 +++++++ gradle.properties | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f80c04a3..0f0134602 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [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) diff --git a/gradle.properties b/gradle.properties index 645f8d101..ad6310e65 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.23.0-dev.1 +version = 5.23.0-dev.2 From bd53955df738bb7b819eb91a3e776e9d2ca5c74a Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Mon, 5 May 2025 15:25:25 +0400 Subject: [PATCH 05/18] fix(YouTube): Simplify litho filtering patch (#4910) --- .../patches/components/LithoFilterPatch.java | 19 +- .../returnyoutubedislike/Fingerprints.kt | 12 - .../ReturnYouTubeDislikePatch.kt | 23 +- .../youtube/misc/litho/filter/Fingerprints.kt | 28 +- .../misc/litho/filter/LithoFilterPatch.kt | 242 +++++++----------- .../patches/youtube/shared/Fingerprints.kt | 15 ++ 6 files changed, 145 insertions(+), 194 deletions(-) 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/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 21a87879b..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,18 +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 elementConfigFingerprint = fingerprint { - strings(" enableDroppedFrameLogging", " elementDepthInTree") -} - 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 7fb171646..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 @@ -7,30 +7,24 @@ import app.revanced.patcher.extensions.InstructionExtensions.addInstructions 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.PatchException import app.revanced.patcher.patch.bytecodePatch -import app.revanced.patcher.util.proxy.mutableTypes.MutableField.Companion.toMutable -import app.revanced.patches.youtube.layout.returnyoutubedislike.conversionContextFingerprint 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.indexOfFirstInstructionReversed 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.FiveRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction import com.android.tools.smali.dexlib2.iface.reference.FieldReference import com.android.tools.smali.dexlib2.iface.reference.MethodReference -import com.android.tools.smali.dexlib2.immutable.ImmutableField lateinit var addLithoFilter: (String) -> Unit private set @@ -65,42 +59,27 @@ val lithoFilterPatch = bytecodePatch( * The following pseudocode shows how this patch works: * * class SomeOtherClass { - * // Called before ComponentContextParser.readComponentIdentifier(...) method. + * // Called before ComponentContextParser.parseComponent() method. * public void someOtherMethod(ByteBuffer byteBuffer) { * ExtensionClass.setProtoBuffer(byteBuffer); // Inserted by this patch. * ... * } * } * - * When patching 19.16: - * * class ComponentContextParser { - * public Component readComponentIdentifier(...) { + * 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; * } - * return originalUnpatchedComponent; - * } - * } - * - * When patching 19.18 and later: - * - * class ComponentContextParser { - * public ComponentIdentifierObj readComponentIdentifier(...) { - * ... - * if (extensionClass.filter(identifier, pathBuilder)) { // Inserted by this patch. - * this.patch_isFiltered = true; - * } - * ... - * } - * - * public Component parseBytesToComponentContext(...) { - * ... - * if (this.patch_isFiltered) { // Inserted by this patch. - * return emptyComponent; - * } - * return originalUnpatchedComponent; + * return originalUnpatchedComponent; // Original code. * } * } */ @@ -115,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 """, @@ -134,134 +113,95 @@ val lithoFilterPatch = bytecodePatch( // region Hook the method that parses bytes into a ComponentContext. - // 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() - - // Add a field to store the result of the filtering. This allows checking the field - // just before returning so the original code always runs the same when filtering occurs. - val lithoFilterResultField = ImmutableField( - componentContextParserFingerprint.classDef.type, - "patch_isFiltered", - "Z", - AccessFlags.PRIVATE.value, - null, - null, - null, - ).toMutable() - componentContextParserFingerprint.classDef.fields.add(lithoFilterResultField) - - // Returns an empty component instead of the original component. - fun returnEmptyComponentInstructions(free: Int): String = """ - move-object/from16 v$free, p0 - iget-boolean v$free, v$free, $lithoFilterResultField - if-eqz v$free, :unfiltered - - move-object/from16 v$free, p1 - invoke-static { v$free }, $builderMethodDescriptor - move-result-object v$free - iget-object v$free, v$free, $emptyComponentField - return-object v$free - - :unfiltered - nop - """ - + // 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) { - findInstructionIndicesReversedOrThrow(Opcode.RETURN_OBJECT).forEach { index -> - val free = findFreeRegister(index) + val conversionContextClass = conversionContextFingerprintToString.originalClassDef - addInstructionsAtControlFlowLabel( - index, - returnEmptyComponentInstructions(free) - ) - } - } - } - - // endregion - - // region Read component then store the result. - - readComponentIdentifierFingerprint.method.apply { - val returnIndex = indexOfFirstInstructionReversedOrThrow(Opcode.RETURN_OBJECT) - if (indexOfFirstInstructionReversed(returnIndex - 1, Opcode.RETURN_OBJECT) >= 0) { - throw PatchException("Found multiple return indexes") // Patch needs an update. - } - - val elementConfigClass = elementConfigFingerprint.originalClassDef - val elementConfigClassType = elementConfigClass.type - val elementConfigIndex = indexOfFirstInstructionReversedOrThrow(returnIndex) { - val reference = getReference() - reference?.definingClass == elementConfigClassType - } - val elementConfigStringBuilderField = elementConfigClass.fields.single { field -> - field.type == "Ljava/lang/StringBuilder;" - } - - // Identifier is saved to a field just before the string builder. - val putStringBuilderIndex = indexOfFirstInstructionOrThrow { - val reference = getReference() - opcode == Opcode.IPUT_OBJECT && - reference?.definingClass == elementConfigClassType && - reference.type == "Ljava/lang/StringBuilder;" - } - val elementConfigIdentifierField = getInstruction( - indexOfFirstInstructionReversedOrThrow(putStringBuilderIndex) { + 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() - opcode == Opcode.IPUT_OBJECT && - reference?.definingClass == elementConfigClassType && - reference.type == "Ljava/lang/String;" + reference?.definingClass == conversionContextClass.type + && reference.type == "Ljava/lang/String;" } - ).getReference() + it.method.getInstruction(index).getReference() + } - // Could use some of these free registers multiple times, but this is inserting at a - // return instruction so there is always multiple 4-bit registers available. - val elementConfigRegister = getInstruction(elementConfigIndex).registerC - val identifierRegister = findFreeRegister(returnIndex, elementConfigRegister) - val stringBuilderRegister = findFreeRegister(returnIndex, elementConfigRegister, identifierRegister) - val thisRegister = findFreeRegister(returnIndex, elementConfigRegister, identifierRegister, stringBuilderRegister) - val freeRegister = findFreeRegister(returnIndex, elementConfigRegister, identifierRegister, stringBuilderRegister, thisRegister) + // StringBuilder field for the litho path. + val conversionContextPathBuilderField = conversionContextClass.fields + .single { field -> field.type == "Ljava/lang/StringBuilder;" } - val invokeFilterInstructions = """ - iget-object v$identifierRegister, v$elementConfigRegister, $elementConfigIdentifierField - iget-object v$stringBuilderRegister, v$elementConfigRegister, $elementConfigStringBuilderField - invoke-static { v$identifierRegister, v$stringBuilderRegister }, $EXTENSION_CLASS_DESCRIPTOR->filter(Ljava/lang/String;Ljava/lang/StringBuilder;)Z - move-result v$freeRegister - move-object/from16 v$thisRegister, p0 - iput-boolean v$freeRegister, v$thisRegister, $lithoFilterResultField - """ + val conversionContextResultIndex = indexOfFirstInstructionOrThrow { + val reference = getReference() + reference?.returnType == conversionContextClass.type + } + 1 - if (is_19_18_or_greater) { - addInstructionsAtControlFlowLabel( - returnIndex, - invokeFilterInstructions - ) - } else { - val elementConfigMethod = conversionContextFingerprint.originalClassDef.methods - .single { method -> - !AccessFlags.STATIC.isSet(method.accessFlags) && method.returnType == elementConfigClassType - } + val conversionContextResultRegister = getInstruction( + conversionContextResultIndex + ).registerA + + 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, """ - # Element config is a method on a parameter. - move-object/from16 v$elementConfigRegister, p2 - invoke-virtual { v$elementConfigRegister }, $elementConfigMethod - move-result-object v$elementConfigRegister - - $invokeFilterInstructions - - ${returnEmptyComponentInstructions(freeRegister)} + 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 """ ) } 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") From c7a22bb1d2059102f6a26ab3c9c413dd73b9e6de Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Mon, 5 May 2025 11:28:44 +0000 Subject: [PATCH 06/18] chore: Release v5.23.0-dev.3 [skip ci] # [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)) --- CHANGELOG.md | 7 +++++++ gradle.properties | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f0134602..057566da6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [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) diff --git a/gradle.properties b/gradle.properties index ad6310e65..bc7585dd7 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.23.0-dev.2 +version = 5.23.0-dev.3 From 2793f003689dac97b58c036643ca2d5efc1dd695 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 6 May 2025 11:31:12 +0400 Subject: [PATCH 07/18] chore: Sync translations (#4915) --- .../addresources/values-fi-rFI/strings.xml | 29 +++-- .../addresources/values-ja-rJP/strings.xml | 10 +- .../addresources/values-sr-rCS/strings.xml | 8 +- .../addresources/values-sr-rSP/strings.xml | 8 +- .../addresources/values-uk-rUA/strings.xml | 2 +- .../addresources/values-vi-rVN/strings.xml | 118 +++++++++--------- 6 files changed, 95 insertions(+), 80 deletions(-) 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-ja-rJP/strings.xml b/patches/src/main/resources/addresources/values-ja-rJP/strings.xml index 88b96c63e..200c4cd7e 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 について 広告 代替サムネイル フィード @@ -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によって提供されるデータです。詳細はこちらをタップしてください。 @@ -1311,7 +1311,7 @@ Automotive レイアウト GmsCore の設定 - 最近アカウントのログイン情報を変更した場合は、MicroGをアンインストールして再インストールしてください。 + 最近アカウントのログイン情報を変更した場合は、MicroG をアンインストールして再インストールしてください。 URL リダイレクトを回避する 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..881d8b794 100644 --- a/patches/src/main/resources/addresources/values-uk-rUA/strings.xml +++ b/patches/src/main/resources/addresources/values-uk-rUA/strings.xml @@ -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 From 2e3511d03c8198bbdb9336888df038a33fb3ab8c Mon Sep 17 00:00:00 2001 From: Dawid Krajcarz <80264606+drobotk@users.noreply.github.com> Date: Tue, 6 May 2025 09:31:56 +0200 Subject: [PATCH 08/18] feat(Spotify): Add `Sanitize sharing links` patch (#4829) --- .../privacy/SanitizeSharingLinksPatch.java | 43 ++++++++++++ patches/api/patches.api | 4 ++ .../spotify/misc/UnlockPremiumPatch.kt | 4 +- .../spotify/misc/privacy/Fingerprints.kt | 41 +++++++++++ .../misc/privacy/SanitizeSharingLinksPatch.kt | 70 +++++++++++++++++++ 5 files changed, 160 insertions(+), 2 deletions(-) create mode 100644 extensions/spotify/src/main/java/app/revanced/extension/spotify/misc/privacy/SanitizeSharingLinksPatch.java create mode 100644 patches/src/main/kotlin/app/revanced/patches/spotify/misc/privacy/Fingerprints.kt create mode 100644 patches/src/main/kotlin/app/revanced/patches/spotify/misc/privacy/SanitizeSharingLinksPatch.kt 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/patches/api/patches.api b/patches/api/patches.api index 1efb1246b..df4e58df1 100644 --- a/patches/api/patches.api +++ b/patches/api/patches.api @@ -852,6 +852,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; } 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..74eaa578c 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 @@ -7,7 +7,6 @@ import app.revanced.patcher.extensions.InstructionExtensions.removeInstruction import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction import app.revanced.patcher.fingerprint import app.revanced.patcher.patch.bytecodePatch -import app.revanced.patches.spotify.misc.check.checkEnvironmentPatch 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 @@ -72,9 +71,10 @@ val unlockPremiumPatch = bytecodePatch( 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 } 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 + """ + ) + } +} From 52af71f68aa2572cda65fc21dc1b6daa4b4ae3fb Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 6 May 2025 07:35:22 +0000 Subject: [PATCH 09/18] chore: Release v5.23.0-dev.4 [skip ci] # [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)) --- CHANGELOG.md | 7 +++++++ gradle.properties | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 057566da6..33ab98f28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [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) diff --git a/gradle.properties b/gradle.properties index bc7585dd7..4e3a17ec5 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.23.0-dev.3 +version = 5.23.0-dev.4 From 00aa2000ba2eef15a0dd827c2bd84c2e85c412e0 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Tue, 6 May 2025 04:35:47 -0300 Subject: [PATCH 10/18] fix(Spotify - Unlock Spotify Premium): Remove pop up premium ads (#4842) --- .../spotify/misc/UnlockPremiumPatch.java | 1 + .../patches/spotify/misc/Fingerprints.kt | 45 +++++-- .../spotify/misc/UnlockPremiumPatch.kt | 120 +++++++++++++----- .../spotify/misc/fix/SpoofPackageInfoPatch.kt | 2 +- .../patches/spotify/shared/Fingerprints.kt | 2 +- .../kotlin/app/revanced/util/BytecodeUtils.kt | 10 ++ 6 files changed, 137 insertions(+), 43 deletions(-) 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/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..28098f44e 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,41 @@ 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 { + accessFlags(AccessFlags.PUBLIC) + 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 74eaa578c..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,21 +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.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;" @@ -41,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 @@ -61,12 +69,12 @@ 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") } @@ -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/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/util/BytecodeUtils.kt b/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt index 7005ed47a..1e711271c 100644 --- a/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt +++ b/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt @@ -18,6 +18,7 @@ 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 @@ -169,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]. * From 0cf7a4c6be615ed0a52a6bacf87592f5f43ff575 Mon Sep 17 00:00:00 2001 From: hoodles <207470673+hoo-dles@users.noreply.github.com> Date: Tue, 6 May 2025 00:39:07 -0700 Subject: [PATCH 11/18] feat(Pandora): Add `Disable audio ads` and `Unlimited skips` patch (#4841) Co-authored-by: Nuckyz <61953774+Nuckyz@users.noreply.github.com> --- patches/api/patches.api | 8 +++++ .../pandora/ads/DisableAudioAdsPatch.kt | 30 ++++++++++++++++++ .../pandora/misc/EnableUnlimitedSkipsPatch.kt | 31 +++++++++++++++++++ .../patches/pandora/shared/Fingerprints.kt | 7 +++++ 4 files changed, 76 insertions(+) create mode 100644 patches/src/main/kotlin/app/revanced/patches/pandora/ads/DisableAudioAdsPatch.kt create mode 100644 patches/src/main/kotlin/app/revanced/patches/pandora/misc/EnableUnlimitedSkipsPatch.kt create mode 100644 patches/src/main/kotlin/app/revanced/patches/pandora/shared/Fingerprints.kt diff --git a/patches/api/patches.api b/patches/api/patches.api index df4e58df1..6bc936afb 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; } 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") +} From bb672c4674ddc201b8b2648c3906cfc31ef43f10 Mon Sep 17 00:00:00 2001 From: hoodles <207470673+hoo-dles@users.noreply.github.com> Date: Tue, 6 May 2025 00:40:45 -0700 Subject: [PATCH 12/18] feat(Prime Video): Add `Skip ads` patch (#4824) --- extensions/primevideo/build.gradle.kts | 4 ++ .../primevideo/src/main/AndroidManifest.xml | 1 + .../primevideo/ads/SkipAdsPatch.java | 36 +++++++++++++++ extensions/primevideo/stub/build.gradle.kts | 17 +++++++ .../stub/src/main/AndroidManifest.xml | 1 + .../com/amazon/avod/fsm/SimpleTrigger.java | 6 +++ .../java/com/amazon/avod/fsm/StateBase.java | 7 +++ .../java/com/amazon/avod/fsm/Trigger.java | 4 ++ .../java/com/amazon/avod/media/TimeSpan.java | 7 +++ .../com/amazon/avod/media/ads/AdBreak.java | 7 +++ .../ads/internal/state/AdBreakState.java | 4 ++ .../ads/internal/state/AdBreakTrigger.java | 18 ++++++++ .../state/AdEnabledPlaybackState.java | 8 ++++ .../state/AdEnabledPlayerTriggerType.java | 5 +++ .../state/ServerInsertedAdBreakState.java | 4 ++ .../avod/media/playback/VideoPlayer.java | 7 +++ .../media/playback/state/PlayerStateType.java | 4 ++ .../state/trigger/PlayerTriggerType.java | 4 ++ patches/api/patches.api | 8 ++++ .../patches/primevideo/ads/Fingerprints.kt | 33 ++++++++++++++ .../patches/primevideo/ads/SkipAdsPatch.kt | 45 +++++++++++++++++++ .../misc/extension/ExtensionPatch.kt | 5 +++ .../primevideo/misc/extension/Hooks.kt | 9 ++++ 23 files changed, 244 insertions(+) create mode 100644 extensions/primevideo/build.gradle.kts create mode 100644 extensions/primevideo/src/main/AndroidManifest.xml create mode 100644 extensions/primevideo/src/main/java/app/revanced/extension/primevideo/ads/SkipAdsPatch.java create mode 100644 extensions/primevideo/stub/build.gradle.kts create mode 100644 extensions/primevideo/stub/src/main/AndroidManifest.xml create mode 100644 extensions/primevideo/stub/src/main/java/com/amazon/avod/fsm/SimpleTrigger.java create mode 100644 extensions/primevideo/stub/src/main/java/com/amazon/avod/fsm/StateBase.java create mode 100644 extensions/primevideo/stub/src/main/java/com/amazon/avod/fsm/Trigger.java create mode 100644 extensions/primevideo/stub/src/main/java/com/amazon/avod/media/TimeSpan.java create mode 100644 extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/AdBreak.java create mode 100644 extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/internal/state/AdBreakState.java create mode 100644 extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/internal/state/AdBreakTrigger.java create mode 100644 extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/internal/state/AdEnabledPlaybackState.java create mode 100644 extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/internal/state/AdEnabledPlayerTriggerType.java create mode 100644 extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/internal/state/ServerInsertedAdBreakState.java create mode 100644 extensions/primevideo/stub/src/main/java/com/amazon/avod/media/playback/VideoPlayer.java create mode 100644 extensions/primevideo/stub/src/main/java/com/amazon/avod/media/playback/state/PlayerStateType.java create mode 100644 extensions/primevideo/stub/src/main/java/com/amazon/avod/media/playback/state/trigger/PlayerTriggerType.java create mode 100644 patches/src/main/kotlin/app/revanced/patches/primevideo/ads/Fingerprints.kt create mode 100644 patches/src/main/kotlin/app/revanced/patches/primevideo/ads/SkipAdsPatch.kt create mode 100644 patches/src/main/kotlin/app/revanced/patches/primevideo/misc/extension/ExtensionPatch.kt create mode 100644 patches/src/main/kotlin/app/revanced/patches/primevideo/misc/extension/Hooks.kt 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/patches/api/patches.api b/patches/api/patches.api index 6bc936afb..7e9a01a9c 100644 --- a/patches/api/patches.api +++ b/patches/api/patches.api @@ -420,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; } 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;") + } +} From bc2575b0f920155465293585245a9b4118418ea2 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 6 May 2025 07:44:30 +0000 Subject: [PATCH 13/18] chore: Release v5.23.0-dev.5 [skip ci] # [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)) --- CHANGELOG.md | 13 +++++++++++++ gradle.properties | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 33ab98f28..de4e05999 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +# [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) diff --git a/gradle.properties b/gradle.properties index 4e3a17ec5..1e7d50acf 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.23.0-dev.4 +version = 5.23.0-dev.5 From c3bab89fc4189e38c10eee0caa36289de7e29dfa Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Tue, 6 May 2025 12:47:06 +0200 Subject: [PATCH 14/18] fix: Correct incorrect fingerprint --- .../kotlin/app/revanced/patches/spotify/misc/Fingerprints.kt | 1 - 1 file changed, 1 deletion(-) 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 28098f44e..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 @@ -82,7 +82,6 @@ internal val homeStructureGetSectionsFingerprint = fingerprint { } internal fun reactivexFunctionApplyWithClassInitFingerprint(className: String) = fingerprint { - accessFlags(AccessFlags.PUBLIC) returns("Ljava/lang/Object;") parameters("Ljava/lang/Object;") custom { method, _ -> method.name == "apply" && method.indexOfFirstInstruction { From bf33b4dae13ca7c08288aff21a2c34a41bec39b8 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 6 May 2025 10:50:54 +0000 Subject: [PATCH 15/18] chore: Release v5.23.0-dev.6 [skip ci] # [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)) --- CHANGELOG.md | 7 +++++++ gradle.properties | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index de4e05999..cbb149b50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [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) diff --git a/gradle.properties b/gradle.properties index 1e7d50acf..1337a50ab 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.23.0-dev.5 +version = 5.23.0-dev.6 From 49ca3290a726cdba7bc9b62ffcd8d46e6f04778e Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Tue, 6 May 2025 09:09:54 -0300 Subject: [PATCH 16/18] fix: Fix incorrect fingerprints (#4917) --- patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt b/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt index 1e711271c..d65a95eaa 100644 --- a/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt +++ b/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt @@ -504,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) } @@ -570,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) } From 35637c8bc78c64b64d43f9982e19b845e15334c8 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 6 May 2025 12:13:29 +0000 Subject: [PATCH 17/18] chore: Release v5.23.0-dev.7 [skip ci] # [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)) --- CHANGELOG.md | 7 +++++++ gradle.properties | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cbb149b50..5ca60dc40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [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) diff --git a/gradle.properties b/gradle.properties index 1337a50ab..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.23.0-dev.6 +version = 5.23.0-dev.7 From fffd416902aa41bf9d90a7f34fb3617842d2d50e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 10 May 2025 12:39:30 +0400 Subject: [PATCH 18/18] chore: Sync translations (#4933) --- .../addresources/values-ga-rIE/strings.xml | 14 +-- .../addresources/values-ja-rJP/strings.xml | 38 ++++---- .../addresources/values-ko-rKR/strings.xml | 2 +- .../addresources/values-uk-rUA/strings.xml | 86 +++++++++---------- 4 files changed, 70 insertions(+), 70 deletions(-) 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 200c4cd7e..2ab5925f6 100644 --- a/patches/src/main/resources/addresources/values-ja-rJP/strings.xml +++ b/patches/src/main/resources/addresources/values-ja-rJP/strings.xml @@ -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> @@ -1099,11 +1099,11 @@ MicroG GmsCore に対する電池の最適化を無効にしても、バッテ "変更点は以下のとおりです: タブレット レイアウト -• コミュニティ投稿が表示されません +• コミュニティ投稿が表示されない Automotive レイアウト -• ショート動画を通常のプレーヤーで開きます -• フィードがトピックとチャンネルで分類されます" +• ショート動画を通常のプレーヤーで開く +• フィードがトピックとチャンネルで分類される" アプリのバージョンを偽装する @@ -1189,8 +1189,8 @@ Automotive レイアウト 「ダブルタップとピンチでサイズ変更」を有効にする "「ダブルタップとピンチでサイズ変更」は有効です -• ダブルタップでミニプレーヤーのサイズを大きくします -• もう一度ダブルタップすると、元のサイズに戻ります" +• ダブルタップすると、ミニプレーヤーのサイズが大きくなる +• もう一度ダブルタップすると、元のサイズに戻る" 「 ダブルタップとピンチでサイズ変更」は無効です ドラッグ&ドロップを有効にする "ドラッグ&ドロップは有効です @@ -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-uk-rUA/strings.xml b/patches/src/main/resources/addresources/values-uk-rUA/strings.xml index 881d8b794..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. Натисніть тут, щоб дізнатися більше та побачити завантаження для інших платформ Макет інтерфейсу