diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/ShortsAutoplayPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/ShortsAutoplayPatch.java index bd650b243..9e5aff20b 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/ShortsAutoplayPatch.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/ShortsAutoplayPatch.java @@ -2,6 +2,8 @@ package app.revanced.extension.youtube.patches; import android.app.Activity; +import androidx.annotation.Nullable; + import java.lang.ref.WeakReference; import java.util.Objects; @@ -76,7 +78,7 @@ public class ShortsAutoplayPatch { /** * Injection point. */ - public static Enum changeShortsRepeatBehavior(Enum original) { + public static Enum changeShortsRepeatBehavior(@Nullable Enum original) { try { final boolean autoplay; @@ -98,17 +100,35 @@ public class ShortsAutoplayPatch { : ShortsLoopBehavior.REPEAT; if (behavior.ytEnumValue != null) { - Logger.printDebug(() -> behavior.ytEnumValue == original - ? "Changing Shorts repeat behavior from: " + original.name() + " to: " + behavior.ytEnumValue - : "Behavior setting is same as original. Using original: " + original.name() - ); + Logger.printDebug(() -> { + String name = (original == null ? "unknown (null)" : original.name()); + return behavior == original + ? "Behavior setting is same as original. Using original: " + name + : "Changing Shorts repeat behavior from: " + name + " to: " + behavior.name(); + }); return behavior.ytEnumValue; } + + if (original == null) { + // Cannot return null, as null is used to indicate Short was auto played. + // Unpatched app replaces null with unknown enum type (appears to fix for bad api data). + Enum unknown = ShortsLoopBehavior.UNKNOWN.ytEnumValue; + Logger.printDebug(() -> "Original is null, returning: " + unknown.name()); + return unknown; + } } catch (Exception ex) { - Logger.printException(() -> "changeShortsRepeatState failure", ex); + Logger.printException(() -> "changeShortsRepeatBehavior failure", ex); } return original; } + + + /** + * Injection point. + */ + public static boolean isAutoPlay(Enum original) { + return ShortsLoopBehavior.SINGLE_PLAY.ytEnumValue == original; + } } diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/shortsautoplay/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/shortsautoplay/Fingerprints.kt index cd48868f5..045d75ca2 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/shortsautoplay/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/shortsautoplay/Fingerprints.kt @@ -1,8 +1,13 @@ package app.revanced.patches.youtube.layout.shortsautoplay import app.revanced.patcher.fingerprint +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.Method +import com.android.tools.smali.dexlib2.iface.reference.FieldReference +import com.android.tools.smali.dexlib2.iface.reference.MethodReference internal val reelEnumConstructorFingerprint = fingerprint { accessFlags(AccessFlags.STATIC, AccessFlags.CONSTRUCTOR) @@ -20,3 +25,27 @@ internal val reelPlaybackRepeatFingerprint = fingerprint { parameters("L") strings("YoutubePlayerState is in throwing an Error.") } + +internal val reelPlaybackFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("V") + parameters("J") + custom { method, _ -> + indexOfMilliSecondsInstruction(method) >= 0 && + indexOfInitializationInstruction(method) >= 0 + } +} + +private fun indexOfMilliSecondsInstruction(method: Method) = + method.indexOfFirstInstruction { + getReference()?.name == "MILLISECONDS" + } + +internal fun indexOfInitializationInstruction(method: Method) = + method.indexOfFirstInstruction { + val reference = getReference() + opcode == Opcode.INVOKE_DIRECT && + reference?.name == "" && + reference.parameterTypes.size == 3 && + reference.parameterTypes.firstOrNull() == "I" + } diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/shortsautoplay/ShortsAutoplayPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/shortsautoplay/ShortsAutoplayPatch.kt index 67bb3c159..d95826f89 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/shortsautoplay/ShortsAutoplayPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/shortsautoplay/ShortsAutoplayPatch.kt @@ -2,21 +2,32 @@ package app.revanced.patches.youtube.layout.shortsautoplay 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.patch.bytecodePatch +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable import app.revanced.patches.all.misc.resources.addResources import app.revanced.patches.shared.misc.mapping.resourceMappingPatch 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_19_34_or_greater +import app.revanced.patches.youtube.misc.playservice.is_20_09_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.patches.youtube.shared.mainActivityOnCreateFingerprint import app.revanced.util.findInstructionIndicesReversedOrThrow import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstructionOrThrow +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.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.ImmutableMethod +import com.android.tools.smali.dexlib2.immutable.ImmutableMethodParameter private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/youtube/patches/ShortsAutoplayPatch;" @@ -98,5 +109,84 @@ val shortsAutoplayPatch = bytecodePatch( ) } } + + // As of YouTube 20.09, Google has removed the code for 'Autoplay' and 'Pause' from this method. + // Manually restore the removed 'Autoplay' code. + if (is_20_09_or_greater) { + // Variable names are only a rough guess of what these methods do. + val userActionMethodIndex = indexOfInitializationInstruction(reelPlaybackFingerprint.method) + val userActionMethodReference = reelPlaybackFingerprint.method + .getInstruction(userActionMethodIndex).reference as MethodReference + val reelSequenceControllerMethodIndex = reelPlaybackFingerprint.method + .indexOfFirstInstructionOrThrow(userActionMethodIndex, Opcode.INVOKE_VIRTUAL) + val reelSequenceControllerMethodReference = reelPlaybackFingerprint.method + .getInstruction(reelSequenceControllerMethodIndex).reference as MethodReference + + reelPlaybackRepeatFingerprint.method.apply { + // Find the first call modified by extension code above. + val extensionReturnResultIndex = indexOfFirstInstructionOrThrow { + opcode == Opcode.INVOKE_STATIC && + getReference()?.definingClass == EXTENSION_CLASS_DESCRIPTOR + } + 1 + val enumRegister = getInstruction(extensionReturnResultIndex).registerA + val getReelSequenceControllerIndex = indexOfFirstInstructionOrThrow(extensionReturnResultIndex) { + val reference = getReference() + opcode == Opcode.IGET_OBJECT && + reference?.definingClass == definingClass && + reference.type == reelSequenceControllerMethodReference.definingClass + } + val getReelSequenceControllerReference = + getInstruction(getReelSequenceControllerIndex).reference + + // Add a helper method to avoid finding multiple free registers. + // If enum is autoplay then method performs autoplay and returns null, + // otherwise returns the same enum. + val helperClass = definingClass + val helperName = "patch_handleAutoPlay" + val helperReturnType = "Ljava/lang/Enum;" + val helperMethod = ImmutableMethod( + helperClass, + helperName, + listOf(ImmutableMethodParameter("Ljava/lang/Enum;", null, null)), + helperReturnType, + AccessFlags.PRIVATE.value, + null, + null, + MutableMethodImplementation(7), + ).toMutable().apply { + addInstructionsWithLabels( + 0, + """ + invoke-static { p1 }, $EXTENSION_CLASS_DESCRIPTOR->isAutoPlay(Ljava/lang/Enum;)Z + move-result v0 + if-eqz v0, :ignore + new-instance v0, ${userActionMethodReference.definingClass} + const/4 v1, 0x3 + const/4 v2, 0x0 + invoke-direct { v0, v1, v2, v2 }, $userActionMethodReference + iget-object v3, p0, $getReelSequenceControllerReference + invoke-virtual { v3, v0 }, $reelSequenceControllerMethodReference + const/4 v4, 0x0 + return-object v4 + :ignore + return-object p1 + """ + ) + } + reelPlaybackRepeatFingerprint.classDef.methods.add(helperMethod) + + addInstructionsWithLabels( + extensionReturnResultIndex + 1, + """ + invoke-direct { p0, v$enumRegister }, $helperClass->$helperName(Ljava/lang/Enum;)$helperReturnType + move-result-object v$enumRegister + if-nez v$enumRegister, :ignore + return-void # Autoplay was performed. + :ignore + nop + """ + ) + } + } } }