diff --git a/src/main/kotlin/app/revanced/patches/youtube/shorts/repeat/ShortsRepeatPatch.kt b/src/main/kotlin/app/revanced/patches/youtube/shorts/repeat/ShortsRepeatPatch.kt new file mode 100644 index 000000000..1e757e5c4 --- /dev/null +++ b/src/main/kotlin/app/revanced/patches/youtube/shorts/repeat/ShortsRepeatPatch.kt @@ -0,0 +1,158 @@ +package app.revanced.patches.youtube.shorts.repeat + +import app.revanced.patcher.data.BytecodeContext +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.patch.BytecodePatch +import app.revanced.patcher.patch.annotation.CompatiblePackage +import app.revanced.patcher.patch.annotation.Patch +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod +import app.revanced.patches.youtube.shorts.repeat.fingerprints.ReelEnumConstructorFingerprint +import app.revanced.patches.youtube.shorts.repeat.fingerprints.ReelEnumStaticFingerprint +import app.revanced.patches.youtube.utils.integrations.Constants.SHORTS +import app.revanced.patches.youtube.utils.settings.SettingsPatch +import app.revanced.patches.youtube.utils.settings.SettingsPatch.contexts +import app.revanced.util.containsReferenceInstructionIndex +import app.revanced.util.copyXmlNode +import app.revanced.util.exception +import app.revanced.util.findMutableMethodOf +import app.revanced.util.getStringInstructionIndex +import app.revanced.util.getTargetIndex +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction + +@Patch( + name = "Change shorts repeat state", + description = "Adds an options for whether shorts should repeat, autoplay, or stop.", + dependencies = [SettingsPatch::class], + compatiblePackages = [ + CompatiblePackage( + "com.google.android.youtube", + [ + "18.29.38", + "18.30.37", + "18.31.40", + "18.32.39", + "18.33.40", + "18.34.38", + "18.35.36", + "18.36.39", + "18.37.36", + "18.38.44", + "18.39.41", + "18.40.34", + "18.41.39", + "18.42.41", + "18.43.45", + "18.44.41", + "18.45.43", + "18.46.45", + "18.48.39", + "18.49.37", + "19.01.34", + "19.02.39" + ] + ) + ] +) +@Suppress("unused") +object ShortsRepeatPatch : BytecodePatch( + setOf(ReelEnumConstructorFingerprint) +) { + override fun execute(context: BytecodeContext) { + + ReelEnumConstructorFingerprint.result?.let { + it.mutableMethod.apply { + ReelEnumStaticFingerprint.resolve(context, it.mutableClass) + + arrayOf( + "REEL_LOOP_BEHAVIOR_END_SCREEN" to "endScreen", + "REEL_LOOP_BEHAVIOR_REPEAT" to "repeat", + "REEL_LOOP_BEHAVIOR_SINGLE_PLAY" to "singlePlay" + ).map { (enumName, fieldName) -> + injectEnum(enumName, fieldName) + } + + val endScreenStringIndex = getStringInstructionIndex("REEL_LOOP_BEHAVIOR_END_SCREEN") + val endScreenReferenceIndex = getTargetIndex(endScreenStringIndex, Opcode.SPUT_OBJECT) + val endScreenReference = getInstruction(endScreenReferenceIndex).reference.toString() + + val enumMethodName = ReelEnumStaticFingerprint.result?.mutableMethod?.name + ?: throw ReelEnumStaticFingerprint.exception + + val enumMethodCall = "$definingClass->$enumMethodName(I)$definingClass" + + context.injectHook(endScreenReference, enumMethodCall) + } + } ?: throw ReelEnumConstructorFingerprint.exception + + /** + * Copy arrays + */ + contexts.copyXmlNode("youtube/shorts/host", "values/arrays.xml", "resources") + + /** + * Add settings + */ + SettingsPatch.addPreference( + arrayOf( + "PREFERENCE: SHORTS_SETTINGS", + "SETTINGS: SHORTS_PLAYER_PARENT", + "SETTINGS: CHANGE_SHORTS_REPEAT_STATE" + ) + ) + + SettingsPatch.updatePatchStatus("Change shorts repeat state") + } + + private fun MutableMethod.injectEnum( + enumName: String, + fieldName: String + ) { + val stringIndex = getStringInstructionIndex(enumName) + val insertIndex = getTargetIndex(stringIndex, Opcode.SPUT_OBJECT) + val insertRegister = getInstruction(insertIndex).registerA + + addInstruction( + insertIndex + 1, + "sput-object v$insertRegister, $SHORTS->$fieldName:Ljava/lang/Enum;" + ) + } + + private fun BytecodeContext.injectHook( + endScreenReference: String, + enumMethodCall: String + ) { + classes.forEach { classDef -> + classDef.methods.filter { method -> + method.parameters.size == 1 + && method.parameters[0].startsWith("L") + && method.returnType == "V" + && method.containsReferenceInstructionIndex(endScreenReference) + }.forEach { targetMethod -> + proxy(classDef) + .mutableClass + .findMutableMethodOf(targetMethod) + .apply { + for ((index, instruction) in implementation!!.instructions.withIndex()) { + if (instruction.opcode != Opcode.INVOKE_STATIC) + continue + if ((instruction as ReferenceInstruction).reference.toString() != enumMethodCall) + continue + + val register = getInstruction(index + 1).registerA + + addInstructions( + index + 2, """ + invoke-static {v$register}, $SHORTS->changeShortsRepeatState(Ljava/lang/Enum;)Ljava/lang/Enum; + move-result-object v$register + """ + ) + } + } + } + } + } +} diff --git a/src/main/kotlin/app/revanced/patches/youtube/shorts/repeat/fingerprints/ReelEnumConstructorFingerprint.kt b/src/main/kotlin/app/revanced/patches/youtube/shorts/repeat/fingerprints/ReelEnumConstructorFingerprint.kt new file mode 100644 index 000000000..8d4c79a68 --- /dev/null +++ b/src/main/kotlin/app/revanced/patches/youtube/shorts/repeat/fingerprints/ReelEnumConstructorFingerprint.kt @@ -0,0 +1,12 @@ +package app.revanced.patches.youtube.shorts.repeat.fingerprints + +import app.revanced.patcher.fingerprint.MethodFingerprint + +object ReelEnumConstructorFingerprint : MethodFingerprint( + returnType = "V", + strings = listOf( + "REEL_LOOP_BEHAVIOR_SINGLE_PLAY", + "REEL_LOOP_BEHAVIOR_REPEAT", + "REEL_LOOP_BEHAVIOR_END_SCREEN" + ) +) diff --git a/src/main/kotlin/app/revanced/patches/youtube/shorts/repeat/fingerprints/ReelEnumStaticFingerprint.kt b/src/main/kotlin/app/revanced/patches/youtube/shorts/repeat/fingerprints/ReelEnumStaticFingerprint.kt new file mode 100644 index 000000000..b3f344f0a --- /dev/null +++ b/src/main/kotlin/app/revanced/patches/youtube/shorts/repeat/fingerprints/ReelEnumStaticFingerprint.kt @@ -0,0 +1,11 @@ +package app.revanced.patches.youtube.shorts.repeat.fingerprints + +import app.revanced.patcher.extensions.or +import app.revanced.patcher.fingerprint.MethodFingerprint +import com.android.tools.smali.dexlib2.AccessFlags + +object ReelEnumStaticFingerprint : MethodFingerprint( + accessFlags = AccessFlags.PUBLIC or AccessFlags.STATIC, + parameters = listOf("I"), + returnType = "L" +) diff --git a/src/main/kotlin/app/revanced/util/BytecodeUtils.kt b/src/main/kotlin/app/revanced/util/BytecodeUtils.kt index ae0165d68..73410e80b 100644 --- a/src/main/kotlin/app/revanced/util/BytecodeUtils.kt +++ b/src/main/kotlin/app/revanced/util/BytecodeUtils.kt @@ -110,6 +110,10 @@ fun Method.containsWideLiteralInstructionIndex(literal: Long) = fun Method.containsMethodReferenceNameInstructionIndex(methodName: String) = getTargetIndexWithMethodReferenceName(methodName) >= 0 +fun Method.containsReferenceInstructionIndex(reference: String) = + getTargetIndexWithReference(reference) >= 0 + + /** * Traverse the class hierarchy starting from the given root class. * @@ -223,11 +227,17 @@ fun MutableMethod.getTargetIndexWithMethodReferenceNameReversed(startIndex: Int, return -1 } -fun MutableMethod.getTargetIndexWithReference(reference: String) -= getTargetIndexWithReference(0, reference) +fun Method.getTargetIndexWithReference(reference: String) = implementation?.let { + it.instructions.indexOfFirst { instruction -> + (instruction as? ReferenceInstruction)?.reference.toString().contains(reference) + } +} ?: -1 -fun MutableMethod.getTargetIndexWithReferenceReversed(reference: String) - = getTargetIndexWithReferenceReversed(implementation!!.instructions.size - 1, reference) +fun MutableMethod.getTargetIndexWithReference(reference: String) = + getTargetIndexWithReference(0, reference) + +fun MutableMethod.getTargetIndexWithReferenceReversed(reference: String) = + getTargetIndexWithReferenceReversed(implementation!!.instructions.size - 1, reference) fun MutableMethod.getTargetIndexWithReference(startIndex: Int, reference: String) = implementation!!.instructions.let { diff --git a/src/main/resources/youtube/settings/host/values/strings.xml b/src/main/resources/youtube/settings/host/values/strings.xml index e2c4335cf..4d216146f 100644 --- a/src/main/resources/youtube/settings/host/values/strings.xml +++ b/src/main/resources/youtube/settings/host/values/strings.xml @@ -58,6 +58,11 @@ Tap here to learn more about DeArrow." Ambient mode is disabled in battery saver mode. Ambient mode is enabled in battery saver mode. Bypass ambient mode restrictions + Autoplay + Default + Pause + Repeat + Change shorts repeat state Switch toggles are used. Text toggles are used. Change toggle type @@ -142,7 +147,6 @@ Note: This feature hasn't been tested." Shorts player will resume on app startup Shorts player will not resume on app startup Disable resuming Shorts player - "Disable 'Playing at 2x speed' while holding down. Note: Disabling the speed overlay restores the 'Slide to seek' behavior of the old layout." diff --git a/src/main/resources/youtube/settings/xml/revanced_prefs.xml b/src/main/resources/youtube/settings/xml/revanced_prefs.xml index 743cfb625..4a84112da 100644 --- a/src/main/resources/youtube/settings/xml/revanced_prefs.xml +++ b/src/main/resources/youtube/settings/xml/revanced_prefs.xml @@ -508,6 +508,7 @@ + @@ -686,6 +687,9 @@ + + diff --git a/src/main/resources/youtube/shorts/host/values/arrays.xml b/src/main/resources/youtube/shorts/host/values/arrays.xml new file mode 100644 index 000000000..5daf00615 --- /dev/null +++ b/src/main/resources/youtube/shorts/host/values/arrays.xml @@ -0,0 +1,15 @@ + + + + @string/revanced_change_shorts_repeat_state_entry_default + @string/revanced_change_shorts_repeat_state_entry_repeat + @string/revanced_change_shorts_repeat_state_entry_auto_play + @string/revanced_change_shorts_repeat_state_entry_pause + + + 0 + 1 + 2 + 3 + +