diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/ForceOriginalAudioPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/ForceOriginalAudioPatch.java index 7a713d4f4..c67a5ffec 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/ForceOriginalAudioPatch.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/ForceOriginalAudioPatch.java @@ -24,6 +24,16 @@ public class ForceOriginalAudioPatch { } } + /** + * Injection point. + */ + public static boolean ignoreDefaultAudioStream(boolean original) { + if (Settings.FORCE_ORIGINAL_AUDIO.get()) { + return false; + } + return original; + } + /** * Injection point. */ @@ -50,7 +60,6 @@ public class ForceOriginalAudioPatch { return isOriginal; } catch (Exception ex) { Logger.printException(() -> "isDefaultAudioStream failure", ex); - return isDefault; } } diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/audio/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/audio/Fingerprints.kt index a338ea6b7..8e3755bc8 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/video/audio/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/audio/Fingerprints.kt @@ -1,23 +1,27 @@ package app.revanced.patches.youtube.video.audio import app.revanced.patcher.fingerprint +import app.revanced.util.containsLiteralInstruction import com.android.tools.smali.dexlib2.AccessFlags -internal val streamingModelBuilderFingerprint = fingerprint { +internal val formatStreamModelToStringFingerprint = fingerprint { accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) - returns("L") - strings("vprng") + returns("Ljava/lang/String;") + custom { method, classDef -> + method.name == "toString" && classDef.type == + "Lcom/google/android/libraries/youtube/innertube/model/media/FormatStreamModel;" + } } -internal val menuItemAudioTrackFingerprint = fingerprint { - accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) - parameters("L") - returns("V") - strings("menu_item_audio_track") +internal const val AUDIO_STREAM_IGNORE_DEFAULT_FEATURE_FLAG = 45666189L + +internal val selectAudioStreamFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC) + returns("L") + custom { method, _ -> + method.parameters.size > 2 // Method has a large number of parameters and may change. + && method.parameters[1].type == "Lcom/google/android/libraries/youtube/innertube/model/media/PlayerConfigModel;" + && method.containsLiteralInstruction(AUDIO_STREAM_IGNORE_DEFAULT_FEATURE_FLAG) + } } -internal val audioStreamingTypeSelector = fingerprint { - accessFlags(AccessFlags.PRIVATE, AccessFlags.FINAL) - returns("L") - strings("raw") // String is not unique -} \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/audio/ForceOriginalAudioPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/audio/ForceOriginalAudioPatch.kt index c60c20db5..40110878c 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/video/audio/ForceOriginalAudioPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/audio/ForceOriginalAudioPatch.kt @@ -5,22 +5,22 @@ import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWith import app.revanced.patcher.extensions.InstructionExtensions.getInstruction import app.revanced.patcher.patch.bytecodePatch import app.revanced.patcher.util.proxy.mutableTypes.MutableField.Companion.toMutable -import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable import app.revanced.patches.all.misc.resources.addResources import app.revanced.patches.all.misc.resources.addResourcesPatch import app.revanced.patches.shared.misc.settings.preference.SwitchPreference import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch +import app.revanced.patches.youtube.misc.playservice.is_20_07_or_greater +import app.revanced.patches.youtube.misc.playservice.versionCheckPatch import app.revanced.patches.youtube.misc.settings.PreferenceScreen import app.revanced.patches.youtube.misc.settings.settingsPatch -import app.revanced.util.getReference +import app.revanced.util.findMethodFromToString import app.revanced.util.indexOfFirstInstructionOrThrow +import app.revanced.util.insertLiteralOverride import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation -import com.android.tools.smali.dexlib2.iface.Method import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction -import com.android.tools.smali.dexlib2.iface.reference.MethodReference import com.android.tools.smali.dexlib2.immutable.ImmutableField import com.android.tools.smali.dexlib2.immutable.ImmutableMethod import com.android.tools.smali.dexlib2.immutable.ImmutableMethodParameter @@ -37,6 +37,7 @@ val forceOriginalAudioPatch = bytecodePatch( sharedExtensionPatch, settingsPatch, addResourcesPatch, + versionCheckPatch ) compatibleWith( @@ -60,29 +61,25 @@ val forceOriginalAudioPatch = bytecodePatch( ) ) - fun Method.firstFormatStreamingModelCall( - returnType: String = "Ljava/lang/String;" - ): MutableMethod { - val audioTrackIdIndex = indexOfFirstInstructionOrThrow { - val reference = getReference() - reference?.definingClass == "Lcom/google/android/libraries/youtube/innertube/model/media/FormatStreamModel;" - && reference.returnType == returnType - } - - return navigate(this).to(audioTrackIdIndex).stop() + // Disable feature flag that ignores the default track flag + // and instead overrides to the user region language. + if (is_20_07_or_greater) { + selectAudioStreamFingerprint.method.insertLiteralOverride( + AUDIO_STREAM_IGNORE_DEFAULT_FEATURE_FLAG, + "$EXTENSION_CLASS_DESCRIPTOR->ignoreDefaultAudioStream(Z)Z" + ) } - // Accessor methods of FormatStreamModel have no string constants and - // opcodes are identical to other methods in the same class, - // so must walk from another class that use the methods. - val isDefaultMethod = streamingModelBuilderFingerprint.originalMethod.firstFormatStreamingModelCall("Z") - val audioTrackIdMethod = menuItemAudioTrackFingerprint.originalMethod.firstFormatStreamingModelCall() - val audioTrackDisplayNameMethod = audioStreamingTypeSelector.originalMethod.firstFormatStreamingModelCall() - val formatStreamModelClass = proxy(classes.first { - it.type == audioTrackIdMethod.definingClass - }).mutableClass + val isDefaultAudioTrackMethod = formatStreamModelToStringFingerprint.originalMethod + .findMethodFromToString("isDefaultAudioTrack=") + val audioTrackDisplayNameMethod = formatStreamModelToStringFingerprint.originalMethod + .findMethodFromToString("audioTrackDisplayName=") + val audioTrackIdMethod = formatStreamModelToStringFingerprint.originalMethod + .findMethodFromToString("audioTrackId=") - formatStreamModelClass.apply { + proxy(classes.first { + it.type == audioTrackIdMethod.definingClass + }).mutableClass.apply { // Add a new field to store the override. val helperFieldName = "isDefaultAudioTrackOverride" fields.add( @@ -103,7 +100,7 @@ val forceOriginalAudioPatch = bytecodePatch( // Add a helper method because the isDefaultAudioTrack() has only 2 registers and 3 are needed. val helperMethodClass = type - val helperMethodName = "extension_isDefaultAudioTrack" + val helperMethodName = "patch_isDefaultAudioTrack" val helperMethod = ImmutableMethod( helperMethodClass, helperMethodName, @@ -143,7 +140,7 @@ val forceOriginalAudioPatch = bytecodePatch( methods.add(helperMethod) // Modify isDefaultAudioTrack() to call extension helper method. - isDefaultMethod.apply { + isDefaultAudioTrackMethod.apply { val index = indexOfFirstInstructionOrThrow(Opcode.RETURN) val register = getInstruction(index).registerA diff --git a/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt b/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt index 60c7bdb0e..6883776e6 100644 --- a/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt +++ b/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt @@ -32,7 +32,10 @@ import com.android.tools.smali.dexlib2.iface.instruction.RegisterRangeInstructio import com.android.tools.smali.dexlib2.iface.instruction.ThreeRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.WideLiteralInstruction +import com.android.tools.smali.dexlib2.iface.reference.FieldReference +import com.android.tools.smali.dexlib2.iface.reference.MethodReference import com.android.tools.smali.dexlib2.iface.reference.Reference +import com.android.tools.smali.dexlib2.iface.reference.StringReference import com.android.tools.smali.dexlib2.immutable.ImmutableField import com.android.tools.smali.dexlib2.util.MethodUtil import java.util.EnumSet @@ -171,6 +174,79 @@ internal val Instruction.isBranchInstruction: Boolean internal val Instruction.isReturnInstruction: Boolean get() = this.opcode in returnOpcodes +/** + * Find the instruction index used for a toString() StringBuilder write of a given String name. + * + * @param fieldName The name of the field to find. Partial matches are allowed. + */ +private fun Method.findInstructionIndexFromToString(fieldName: String) : Int { + val stringIndex = indexOfFirstInstruction { + val reference = getReference() + reference?.string?.contains(fieldName) == true + } + if (stringIndex < 0) { + throw IllegalArgumentException("Could not find usage of string: '$fieldName'") + } + val stringRegister = getInstruction(stringIndex).registerA + + // Find use of the string with a StringBuilder. + val stringUsageIndex = indexOfFirstInstruction(stringIndex) { + val reference = getReference() + reference?.definingClass == "Ljava/lang/StringBuilder;" && + (this as? FiveRegisterInstruction)?.registerD == stringRegister + } + if (stringUsageIndex < 0) { + throw IllegalArgumentException("Could not find StringBuilder usage in: $this") + } + + // Find the next usage of StringBuilder, which should be the desired field. + val fieldUsageIndex = indexOfFirstInstruction(stringUsageIndex + 1) { + val reference = getReference() + reference?.definingClass == "Ljava/lang/StringBuilder;" && reference.name == "append" + } + if (fieldUsageIndex < 0) { + // Should never happen. + throw IllegalArgumentException("Could not find StringBuilder append usage in: $this") + } + val fieldUsageRegister = getInstruction(fieldUsageIndex).registerD + + // Look backwards up the method to find the instruction that sets the register. + var fieldSetIndex = indexOfFirstInstructionReversedOrThrow(fieldUsageIndex - 1) { + fieldUsageRegister == writeRegister + } + + // If the field is a method call, then adjust from MOVE_RESULT to the method call. + val fieldSetOpcode = getInstruction(fieldSetIndex).opcode + if (fieldSetOpcode == MOVE_RESULT || + fieldSetOpcode == MOVE_RESULT_WIDE || + fieldSetOpcode == MOVE_RESULT_OBJECT) { + fieldSetIndex-- + } + + return fieldSetIndex +} + +/** + * Find the method used for a toString() StringBuilder write of a given String name. + * + * @param fieldName The name of the field to find. Partial matches are allowed. + */ +context(BytecodePatchContext) +internal fun Method.findMethodFromToString(fieldName: String) : MutableMethod { + val methodUsageIndex = findInstructionIndexFromToString(fieldName) + return navigate(this).to(methodUsageIndex).stop() +} + +/** + * Find the field used for a toString() StringBuilder write of a given String name. + * + * @param fieldName The name of the field to find. Partial matches are allowed. + */ +internal fun Method.findFieldFromToString(fieldName: String) : FieldReference { + val methodUsageIndex = findInstructionIndexFromToString(fieldName) + return getInstruction(methodUsageIndex).getReference()!! +} + /** * Adds public [AccessFlags] and removes private and protected flags (if present). */ @@ -594,7 +670,7 @@ fun Method.indexOfFirstInstructionReversed(targetOpcode: Opcode): Int = indexOfF /** * Get the index of matching instruction, - * starting from and [startIndex] and searching down. + * starting from [startIndex] and searching down. * * @param startIndex Optional starting index to search down from. Searching includes the start index. * @return The index of the instruction. @@ -617,7 +693,7 @@ fun Method.indexOfFirstInstructionReversedOrThrow(targetOpcode: Opcode): Int = i /** * Get the index of matching instruction, - * starting from and [startIndex] and searching down. + * starting from [startIndex] and searching down. * * @param startIndex Optional starting index to search down from. Searching includes the start index. * @return The index of the instruction.