diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/player/ExitFullscreenPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/player/ExitFullscreenPatch.java new file mode 100644 index 000000000..c66afb75d --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/player/ExitFullscreenPatch.java @@ -0,0 +1,46 @@ +package app.revanced.extension.youtube.patches.player; + +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.shared.utils.Utils; +import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.youtube.shared.PlayerType; +import app.revanced.extension.youtube.utils.VideoUtils; + +@SuppressWarnings("unused") +public class ExitFullscreenPatch { + + public enum FullscreenMode { + DISABLED, + PORTRAIT, + LANDSCAPE, + PORTRAIT_LANDSCAPE, + } + + /** + * Injection point. + */ + public static void endOfVideoReached() { + try { + FullscreenMode mode = Settings.EXIT_FULLSCREEN.get(); + if (mode == FullscreenMode.DISABLED) { + return; + } + + if (PlayerType.getCurrent() == PlayerType.WATCH_WHILE_FULLSCREEN) { + if (mode != FullscreenMode.PORTRAIT_LANDSCAPE) { + if (Utils.isLandscapeOrientation()) { + if (mode == FullscreenMode.PORTRAIT) { + return; + } + } else if (mode == FullscreenMode.LANDSCAPE) { + return; + } + } + + Utils.runOnMainThread(VideoUtils::exitFullscreenMode); + } + } catch (Exception ex) { + Logger.printException(() -> "endOfVideoReached failure", ex); + } + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/Settings.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/Settings.java index 593395496..ade8423d5 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/Settings.java +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/Settings.java @@ -38,6 +38,7 @@ import app.revanced.extension.youtube.patches.general.LayoutSwitchPatch.FormFact import app.revanced.extension.youtube.patches.general.MiniplayerPatch; import app.revanced.extension.youtube.patches.general.YouTubeMusicActionsPatch; import app.revanced.extension.youtube.patches.misc.WatchHistoryPatch.WatchHistoryType; +import app.revanced.extension.youtube.patches.player.ExitFullscreenPatch.FullscreenMode; import app.revanced.extension.youtube.patches.shorts.AnimationFeedbackPatch.AnimationType; import app.revanced.extension.youtube.patches.shorts.ShortsRepeatStatePatch.ShortsLoopBehavior; import app.revanced.extension.youtube.patches.utils.PatchStatus; @@ -334,6 +335,7 @@ public class Settings extends BaseSettings { // PreferenceScreen: Player - Fullscreen public static final BooleanSetting DISABLE_ENGAGEMENT_PANEL = new BooleanSetting("revanced_disable_engagement_panel", FALSE, true); + public static final EnumSetting EXIT_FULLSCREEN = new EnumSetting<>("revanced_exit_fullscreen", FullscreenMode.DISABLED); public static final BooleanSetting SHOW_VIDEO_TITLE_SECTION = new BooleanSetting("revanced_show_video_title_section", TRUE, true, parent(DISABLE_ENGAGEMENT_PANEL)); public static final BooleanSetting HIDE_AUTOPLAY_PREVIEW = new BooleanSetting("revanced_hide_autoplay_preview", FALSE, true); public static final BooleanSetting HIDE_LIVE_CHAT_REPLAY_BUTTON = new BooleanSetting("revanced_hide_live_chat_replay_button", FALSE); diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/utils/VideoUtils.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/utils/VideoUtils.java index 2ee1127b7..0a5eb34e6 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/youtube/utils/VideoUtils.java +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/utils/VideoUtils.java @@ -242,6 +242,20 @@ public class VideoUtils extends IntentUtils { return !isExternalDownloaderLaunched.get() && original; } + /** + * Rest of the implementation added by patch. + */ + public static void enterFullscreenMode() { + Logger.printDebug(() -> "Enter fullscreen mode"); + } + + /** + * Rest of the implementation added by patch. + */ + public static void exitFullscreenMode() { + Logger.printDebug(() -> "Exit fullscreen mode"); + } + /** * Rest of the implementation added by patch. */ diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/player/fullscreen/FullscreenComponentsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/player/fullscreen/FullscreenComponentsPatch.kt index e57b0d6c7..e721180e6 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/player/fullscreen/FullscreenComponentsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/player/fullscreen/FullscreenComponentsPatch.kt @@ -15,9 +15,12 @@ import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PAC import app.revanced.patches.youtube.utils.extension.Constants.COMPONENTS_PATH import app.revanced.patches.youtube.utils.extension.Constants.PATCH_STATUS_CLASS_DESCRIPTOR import app.revanced.patches.youtube.utils.extension.Constants.PLAYER_CLASS_DESCRIPTOR +import app.revanced.patches.youtube.utils.extension.Constants.PLAYER_PATH +import app.revanced.patches.youtube.utils.fullscreen.fullscreenButtonHookPatch import app.revanced.patches.youtube.utils.layoutConstructorFingerprint import app.revanced.patches.youtube.utils.mainactivity.mainActivityResolvePatch import app.revanced.patches.youtube.utils.patch.PatchList.FULLSCREEN_COMPONENTS +import app.revanced.patches.youtube.utils.playertype.playerTypeHookPatch import app.revanced.patches.youtube.utils.playservice.is_18_42_or_greater import app.revanced.patches.youtube.utils.playservice.is_19_41_or_greater import app.revanced.patches.youtube.utils.playservice.versionCheckPatch @@ -28,7 +31,10 @@ import app.revanced.patches.youtube.utils.resourceid.sharedResourceIdPatch import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference import app.revanced.patches.youtube.utils.settings.settingsPatch import app.revanced.patches.youtube.utils.youtubeControlsOverlayFingerprint +import app.revanced.patches.youtube.video.information.videoEndMethod +import app.revanced.patches.youtube.video.information.videoInformationPatch import app.revanced.util.Utils.printWarn +import app.revanced.util.addInstructionsAtControlFlowLabel import app.revanced.util.findMethodOrThrow import app.revanced.util.fingerprint.methodOrThrow import app.revanced.util.fingerprint.mutableClassOrThrow @@ -48,6 +54,9 @@ import com.android.tools.smali.dexlib2.iface.reference.MethodReference private const val FILTER_CLASS_DESCRIPTOR = "$COMPONENTS_PATH/QuickActionFilter;" +private const val EXTENSION_EXIT_FULLSCREEN_CLASS_DESCRIPTOR = + "$PLAYER_PATH/ExitFullscreenPatch;" + @Suppress("unused") val fullscreenComponentsPatch = bytecodePatch( FULLSCREEN_COMPONENTS.title, @@ -57,8 +66,11 @@ val fullscreenComponentsPatch = bytecodePatch( dependsOn( settingsPatch, + playerTypeHookPatch, lithoFilterPatch, mainActivityResolvePatch, + fullscreenButtonHookPatch, + videoInformationPatch, sharedResourceIdPatch, versionCheckPatch, ) @@ -106,6 +118,17 @@ val fullscreenComponentsPatch = bytecodePatch( // endregion + // region patch for exit fullscreen + + videoEndMethod.apply { + addInstructionsAtControlFlowLabel( + implementation!!.instructions.lastIndex, + "invoke-static {}, $EXTENSION_EXIT_FULLSCREEN_CLASS_DESCRIPTOR->endOfVideoReached()V", + ) + } + + // endregion + // region patch for hide autoplay preview layoutConstructorFingerprint.methodOrThrow().apply { diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fullscreen/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fullscreen/Fingerprints.kt new file mode 100644 index 000000000..17f94470f --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fullscreen/Fingerprints.kt @@ -0,0 +1,35 @@ +package app.revanced.patches.youtube.utils.fullscreen + +import app.revanced.util.containsLiteralInstruction +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +private const val NEXT_GEN_WATCH_LAYOUT_CLASS_DESCRIPTOR = + "Lcom/google/android/apps/youtube/app/watch/nextgenwatch/ui/NextGenWatchLayout;" + +internal val nextGenWatchLayoutConstructorFingerprint = legacyFingerprint( + name = "nextGenWatchLayoutConstructorFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR, + parameters = listOf("Landroid/content/Context;", "Landroid/util/AttributeSet;", "I"), + opcodes = listOf(Opcode.CHECK_CAST), + customFingerprint = { method, _ -> + method.definingClass == NEXT_GEN_WATCH_LAYOUT_CLASS_DESCRIPTOR + }, +) + +internal val nextGenWatchLayoutFullscreenModeFingerprint = legacyFingerprint( + name = "nextGenWatchLayoutFullscreenModeFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("I"), + opcodes = listOf(Opcode.INVOKE_DIRECT), + customFingerprint = { method, _ -> + method.definingClass == NEXT_GEN_WATCH_LAYOUT_CLASS_DESCRIPTOR && + method.containsLiteralInstruction(32) + }, +) + + diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fullscreen/FullscreenButtonHookPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fullscreen/FullscreenButtonHookPatch.kt new file mode 100644 index 000000000..5cac3b8f6 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fullscreen/FullscreenButtonHookPatch.kt @@ -0,0 +1,135 @@ +package app.revanced.patches.youtube.utils.fullscreen + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.PatchException +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod +import app.revanced.patches.youtube.utils.extension.Constants.EXTENSION_PATH +import app.revanced.patches.youtube.utils.extension.sharedExtensionPatch +import app.revanced.util.addStaticFieldToExtension +import app.revanced.util.findMethodOrThrow +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstructionOrThrow +import app.revanced.util.indexOfFirstInstructionReversedOrThrow +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 +import com.android.tools.smali.dexlib2.iface.reference.MethodReference +import com.android.tools.smali.dexlib2.iface.reference.TypeReference + +private const val EXTENSION_VIDEO_UTILS_CLASS_DESCRIPTOR = + "$EXTENSION_PATH/utils/VideoUtils;" + +internal lateinit var enterFullscreenMethod: MutableMethod + +val fullscreenButtonHookPatch = bytecodePatch( + description = "fullscreenButtonHookPatch" +) { + + dependsOn(sharedExtensionPatch) + + execute { + val (referenceClass, fullscreenActionClass) = with (nextGenWatchLayoutFullscreenModeFingerprint.methodOrThrow()) { + val targetIndex = indexOfFirstInstructionReversedOrThrow { + opcode == Opcode.INVOKE_DIRECT && + getReference()?.parameterTypes?.size == 2 + } + val targetReference = getInstruction(targetIndex).reference as MethodReference + + Pair(targetReference.definingClass, targetReference.parameterTypes[1].toString()) + } + + val (enterFullscreenReference, exitFullscreenReference, opcodeName) = + with (findMethodOrThrow(referenceClass) { parameters == listOf("I") }) { + val enterFullscreenIndex = indexOfFirstInstructionOrThrow { + val reference = getReference() + reference?.returnType == "V" && + reference.definingClass == fullscreenActionClass && + reference.parameterTypes.size == 0 + } + val exitFullscreenIndex = indexOfFirstInstructionReversedOrThrow { + val reference = getReference() + reference?.returnType == "V" && + reference.definingClass == fullscreenActionClass && + reference.parameterTypes.size == 0 + } + + val enterFullscreenReference = + getInstruction(enterFullscreenIndex).reference + val exitFullscreenReference = + getInstruction(exitFullscreenIndex).reference + val opcode = getInstruction(enterFullscreenIndex).opcode + + val enterFullscreenClass = (enterFullscreenReference as MethodReference).definingClass + + enterFullscreenMethod = if (opcode == Opcode.INVOKE_INTERFACE) { + classes.find { classDef -> classDef.interfaces.contains(enterFullscreenClass) } + ?.let { classDef -> + proxy(classDef) + .mutableClass + .methods + .find { method -> method.name == enterFullscreenReference.name } + } ?: throw PatchException("No matching classes: $enterFullscreenClass") + } else { + findMethodOrThrow(enterFullscreenClass) { + name == enterFullscreenReference.name + } + } + + Triple( + enterFullscreenReference, + exitFullscreenReference, + opcode.name + ) + } + + nextGenWatchLayoutConstructorFingerprint.methodOrThrow().apply { + val targetIndex = indexOfFirstInstructionReversedOrThrow { + opcode == Opcode.CHECK_CAST && + getReference()?.type == fullscreenActionClass + } + val targetRegister = getInstruction(targetIndex).registerA + + addInstruction( + targetIndex + 1, + "sput-object v$targetRegister, $EXTENSION_VIDEO_UTILS_CLASS_DESCRIPTOR->fullscreenActionClass:$fullscreenActionClass" + ) + + val enterFullscreenModeSmaliInstructions = + """ + if-eqz v0, :ignore + $opcodeName {v0}, $enterFullscreenReference + :ignore + return-void + """ + + val exitFullscreenModeSmaliInstructions = + """ + if-eqz v0, :ignore + $opcodeName {v0}, $exitFullscreenReference + :ignore + return-void + """ + + addStaticFieldToExtension( + EXTENSION_VIDEO_UTILS_CLASS_DESCRIPTOR, + "enterFullscreenMode", + "fullscreenActionClass", + fullscreenActionClass, + enterFullscreenModeSmaliInstructions, + false + ) + + addStaticFieldToExtension( + EXTENSION_VIDEO_UTILS_CLASS_DESCRIPTOR, + "exitFullscreenMode", + "fullscreenActionClass", + fullscreenActionClass, + exitFullscreenModeSmaliInstructions, + false + ) + } + } +} diff --git a/patches/src/main/resources/youtube/settings/host/values/arrays.xml b/patches/src/main/resources/youtube/settings/host/values/arrays.xml index 904839887..55d4d5583 100644 --- a/patches/src/main/resources/youtube/settings/host/values/arrays.xml +++ b/patches/src/main/resources/youtube/settings/host/values/arrays.xml @@ -114,6 +114,18 @@ 1440 2160 + + @string/revanced_exit_fullscreen_entry_1 + @string/revanced_exit_fullscreen_entry_2 + @string/revanced_exit_fullscreen_entry_3 + @string/revanced_exit_fullscreen_entry_4 + + + DISABLED + PORTRAIT + LANDSCAPE + PORTRAIT_LANDSCAPE + NewPipe Seal diff --git a/patches/src/main/resources/youtube/settings/host/values/strings.xml b/patches/src/main/resources/youtube/settings/host/values/strings.xml index 6a4648ebe..d12e3b47b 100644 --- a/patches/src/main/resources/youtube/settings/host/values/strings.xml +++ b/patches/src/main/resources/youtube/settings/host/values/strings.xml @@ -923,6 +923,11 @@ Settings → Autoplay → Autoplay next video" Disable engagement panel Engagement panel is disabled. Engagement panel is enabled. + Exit fullscreen mode at end of video + Disabled + Portrait + Landscape + Portrait and landscape Show video title section "Shows the video title section in fullscreen. diff --git a/patches/src/main/resources/youtube/settings/xml/revanced_prefs.xml b/patches/src/main/resources/youtube/settings/xml/revanced_prefs.xml index 43903afc8..c91f850ad 100644 --- a/patches/src/main/resources/youtube/settings/xml/revanced_prefs.xml +++ b/patches/src/main/resources/youtube/settings/xml/revanced_prefs.xml @@ -397,6 +397,7 @@