Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* Copyright 2026 Morphe.
* https://github.com/MorpheApp/morphe-patches
*
* See the included NOTICE file for GPLv3 §7(b) and §7(c) terms that apply to this code.
*/

package app.morphe.extension.music.patches;

import app.morphe.extension.music.settings.Settings;

@SuppressWarnings("unused")
public class EnableSwipeToDismissMiniplayerPatch {

/**
* Injection point
*/
public static boolean enableSwipeToDismissMiniplayer() {
return Settings.ENABLE_SWIPE_TO_DISMISS_MINIPLAYER.get();
}

/**
* Injection point
*/
public static Object enableSwipeToDismissMiniplayer(Object object) {
return Settings.ENABLE_SWIPE_TO_DISMISS_MINIPLAYER.get() ? null : object;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ public class Settings extends SharedYouTubeSettings {
public static final BooleanSetting MINIPLAYER_PREVIOUS_BUTTON = new BooleanSetting("morphe_music_miniplayer_previous_button", TRUE, true);
public static final BooleanSetting CHANGE_MINIPLAYER_COLOR = new BooleanSetting("morphe_music_change_miniplayer_color", FALSE, true);
public static final BooleanSetting ENABLE_FORCED_MINIPLAYER = new BooleanSetting("morphe_music_enable_forced_miniplayer", FALSE, true);
public static final BooleanSetting ENABLE_SWIPE_TO_DISMISS_MINIPLAYER = new BooleanSetting("morphe_music_enable_swipe_to_dismiss_miniplayer", FALSE, true);
public static final BooleanSetting PERMANENT_REPEAT = new BooleanSetting("morphe_music_play_permanent_repeat", FALSE, true);

// Crossfade
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,8 +162,8 @@ val crossfadePatch = bytecodePatch(
val log = Logger.getLogger(this::class.java.name)
if (!is_8_05_or_greater || is_9_00_or_greater) {
return@execute log.warning(
"Track crossfade is not yet available for YouTube Music 9.x. " +
"Patch YouTube Music 8.44.548.50.51 for crossfade.",
"Track crossfade is not supported on YouTube Music 9.x. " +
"Please patch versions 8.44.54 through 8.50.51 for this feature.",
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
private const val EXTENSION_CLASS = "Lapp/morphe/extension/music/patches/HideButtonsPatch;"

@Suppress("unused")
val hideButtons = bytecodePatch(
val hideButtonsPatch = bytecodePatch(
name = "Hide buttons",
description = "Adds options to hide the cast, history, notification, and search buttons."
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
private const val EXTENSION_CLASS = "Lapp/morphe/extension/music/patches/HideCategoryBarPatch;"

@Suppress("unused")
val hideCategoryBar = bytecodePatch(
val hideCategoryBarPatch = bytecodePatch(
name = "Hide category bar",
description = "Adds an option to hide the category bar at the top of the homepage."
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import com.android.tools.smali.dexlib2.iface.reference.MethodReference
private const val EXTENSION_CLASS = "Lapp/morphe/extension/music/patches/ChangeMiniplayerColorPatch;"

@Suppress("unused")
val changeMiniplayerColor = bytecodePatch(
val changeMiniplayerColorPatch = bytecodePatch(
name = "Change miniplayer color",
description = "Adds an option to change the miniplayer background color to match the fullscreen player."
) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
/*
* Copyright 2026 Morphe.
* https://github.com/MorpheApp/morphe-patches
*
* See the included NOTICE file for GPLv3 §7(b) and §7(c) terms that apply to this code.
*/

package app.morphe.patches.music.layout.miniplayer

import app.morphe.patcher.extensions.InstructionExtensions.addInstructions
import app.morphe.patcher.extensions.InstructionExtensions.getInstruction
import app.morphe.patcher.extensions.InstructionExtensions.replaceInstruction
import app.morphe.patcher.patch.bytecodePatch
import app.morphe.patches.music.misc.extension.sharedExtensionPatch
import app.morphe.patches.music.misc.settings.PreferenceScreen
import app.morphe.patches.music.misc.settings.settingsPatch
import app.morphe.patches.music.shared.Constants.COMPATIBILITY_YOUTUBE_MUSIC
import app.morphe.patches.shared.misc.settings.preference.SwitchPreference
import app.morphe.util.addInstructionsAtControlFlowLabel
import app.morphe.util.findFreeRegister
import app.morphe.util.getReference
import app.morphe.util.indexOfFirstInstructionOrThrow
import app.morphe.util.indexOfFirstInstructionReversedOrThrow
import app.morphe.util.indexOfFirstLiteralInstructionOrThrow
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction10x
import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction
import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
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.Reference
import com.android.tools.smali.dexlib2.iface.reference.StringReference

private const val EXTENSION_CLASS = "Lapp/morphe/extension/music/patches/EnableSwipeToDismissMiniplayerPatch;"

@Suppress("unused")
val enableSwipeToDismissMiniplayerPatch = bytecodePatch(
name = "Enable swipe to dismiss miniplayer",
description = "Adds an option to enable dismissing the miniplayer by swiping down on it."
) {
dependsOn(
sharedExtensionPatch,
settingsPatch
)

compatibleWith(COMPATIBILITY_YOUTUBE_MUSIC)

execute {
PreferenceScreen.PLAYER.addPreferences(
SwitchPreference("morphe_music_enable_swipe_to_dismiss_miniplayer")
)

val swipeToDismissSGetObjectReference = InteractionLoggingEnumFingerprint.method.let { m ->
val stringIndex = m.indexOfFirstInstructionOrThrow { getReference<StringReference>()?.string == "INTERACTION_LOGGING_GESTURE_TYPE_SWIPE" }
val sPutObjectIndex = m.indexOfFirstInstructionOrThrow(stringIndex, Opcode.SPUT_OBJECT)
m.getInstruction<ReferenceInstruction>(sPutObjectIndex).reference
}

val musicActivityWidgetMethod = MusicActivityWidgetFingerprint.method
val swipeToDismissWidgetIndex = musicActivityWidgetMethod.indexOfFirstLiteralInstructionOrThrow(79500L)

fun getSwipeToDismissReference(targetOpcode: Opcode, reversed: Boolean): Reference {
val targetIndex = if (reversed)
musicActivityWidgetMethod.indexOfFirstInstructionReversedOrThrow(swipeToDismissWidgetIndex) {
opcode == targetOpcode
}
else
musicActivityWidgetMethod.indexOfFirstInstructionOrThrow(swipeToDismissWidgetIndex, targetOpcode)

return musicActivityWidgetMethod.getInstruction<ReferenceInstruction>(targetIndex).reference
}

val swipeToDismissIGetObjectReference = getSwipeToDismissReference(Opcode.IGET_OBJECT, true)
val swipeToDismissInvokeInterfacePrimaryReference = getSwipeToDismissReference(Opcode.INVOKE_INTERFACE, true)
val swipeToDismissCheckCastReference = getSwipeToDismissReference(Opcode.CHECK_CAST, true)
val swipeToDismissNewInstanceReference = getSwipeToDismissReference(Opcode.NEW_INSTANCE, true)
val swipeToDismissInvokeStaticReference = getSwipeToDismissReference(Opcode.INVOKE_STATIC, false)
val swipeToDismissInvokeDirectReference = getSwipeToDismissReference(Opcode.INVOKE_DIRECT, false)
val swipeToDismissInvokeInterfaceSecondaryReference = getSwipeToDismissReference(Opcode.INVOKE_INTERFACE, false)
val dismissBehaviorMethodRef = HandleSignInEventFingerprint.method.let { m ->
val returnIndex = m.indexOfFirstInstructionOrThrow(Opcode.RETURN_VOID)
val invokeIndex = m.indexOfFirstInstructionReversedOrThrow(returnIndex, Opcode.INVOKE_VIRTUAL)

m.getInstruction<ReferenceInstruction>(invokeIndex).reference as MethodReference
}

val dismissBehaviorMethod = mutableClassDefBy(dismissBehaviorMethodRef.definingClass).methods.single {
it.name == dismissBehaviorMethodRef.name && it.parameters == dismissBehaviorMethodRef.parameterTypes && it.returnType == dismissBehaviorMethodRef.returnType
}

dismissBehaviorMethod.apply {
val insertIndex = indexOfFirstInstructionOrThrow {
getReference<FieldReference>()?.type == "Ljava/util/concurrent/atomic/AtomicBoolean;"
}
val primaryRegister = getInstruction<TwoRegisterInstruction>(insertIndex).registerB
val freeRegister = findFreeRegister(insertIndex, primaryRegister)
val totalRegs = implementation!!.registerCount
val clobberRegs = (0 until totalRegs).filter { it != primaryRegister }

if (clobberRegs.size < 3) {
throw IllegalStateException("Method lacks sufficient registers for injection (total: ${totalRegs})")
}

val secondaryRegister = clobberRegs[0]
val tertiaryRegister = clobberRegs[1]
val nullRegister = clobberRegs[2]

addInstructionsAtControlFlowLabel(
insertIndex, """
invoke-static {}, $EXTENSION_CLASS->enableSwipeToDismissMiniplayer()Z
move-result v$freeRegister
if-nez v$freeRegister, :dismiss

# We are safe to aggressively clobber inside here
iget-object v$primaryRegister, v$primaryRegister, $swipeToDismissIGetObjectReference
invoke-interface {v$primaryRegister}, $swipeToDismissInvokeInterfacePrimaryReference
move-result-object v$primaryRegister
check-cast v$primaryRegister, $swipeToDismissCheckCastReference

sget-object v$secondaryRegister, $swipeToDismissSGetObjectReference
new-instance v$tertiaryRegister, $swipeToDismissNewInstanceReference

const v$nullRegister, 0x878b
invoke-static {v$nullRegister}, $swipeToDismissInvokeStaticReference
move-result-object v$nullRegister
invoke-direct {v$tertiaryRegister, v$nullRegister}, $swipeToDismissInvokeDirectReference

const/4 v$nullRegister, 0x0
invoke-interface {v$primaryRegister, v$secondaryRegister, v$tertiaryRegister, v$nullRegister}, $swipeToDismissInvokeInterfaceSecondaryReference
return-void

:dismiss
nop
"""
)
}

MiniPlayerDefaultTextFingerprint.method.apply {
if (parameters.isEmpty()) {
addInstructions(0, """
invoke-static {}, $EXTENSION_CLASS->enableSwipeToDismissMiniplayer()Z
move-result v0
if-eqz v0, :continue_exec
return-void
:continue_exec
""")
} else {
val insertIndex = indexOfFirstInstructionOrThrow(Opcode.IF_NE)
val insertRegister = getInstruction<TwoRegisterInstruction>(insertIndex).registerB

addInstructions(insertIndex, """
invoke-static {v$insertRegister}, $EXTENSION_CLASS->enableSwipeToDismissMiniplayer(Ljava/lang/Object;)Ljava/lang/Object;
move-result-object v$insertRegister
""")
}
}

val targetMethod = MiniPlayerDefaultViewVisibilityFingerprint.classDef.methods.first {
it.parameters == listOf("Landroid/view/View;", "I")
}

targetMethod.apply {
val bottomSheetBehaviorIndex = indexOfFirstInstructionOrThrow {
val reference = getReference<MethodReference>()
opcode == Opcode.INVOKE_VIRTUAL &&
reference?.definingClass == "Lcom/google/android/material/bottomsheet/BottomSheetBehavior;" &&
reference.parameterTypes.firstOrNull() == "Z"
}

val invokeInstruction = getInstruction<FiveRegisterInstruction>(bottomSheetBehaviorIndex)
val invokeReference = (invokeInstruction as ReferenceInstruction).reference as MethodReference
val registerC = invokeInstruction.registerC
val registerD = invokeInstruction.registerD
replaceInstruction(bottomSheetBehaviorIndex, BuilderInstruction10x(Opcode.NOP))

addInstructionsAtControlFlowLabel(
bottomSheetBehaviorIndex, """
invoke-static {}, $EXTENSION_CLASS->enableSwipeToDismissMiniplayer()Z
move-result v$registerD
if-nez v$registerD, :skip_invoke
invoke-virtual {v$registerC, v$registerD}, $invokeReference
:skip_invoke
nop
"""
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import app.morphe.patcher.opcode
import app.morphe.patcher.string
import app.morphe.patches.all.misc.resources.ResourceType
import app.morphe.patches.all.misc.resources.resourceLiteral
import app.morphe.util.indexOfFirstLiteralInstruction
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode

Expand Down Expand Up @@ -65,3 +66,48 @@ internal object MppWatchWhileLayoutFingerprint : Fingerprint(
returnType = "V",
parameters = listOf(),
)

internal object InteractionLoggingEnumFingerprint : Fingerprint(
returnType = "V",
strings = listOf("INTERACTION_LOGGING_GESTURE_TYPE_SWIPE")
)

internal object MusicActivityWidgetFingerprint : Fingerprint(
custom = { method, classDef ->
classDef.type.endsWith("/MusicActivity;") &&
method.indexOfFirstLiteralInstruction(79500L) >= 0
}
)

internal object HandleSearchRenderedFingerprint : Fingerprint(
returnType = "V",
name = "handleSearchRendered"
)

internal object HandleSignInEventFingerprint : Fingerprint(
classFingerprint = HandleSearchRenderedFingerprint,
returnType = "V",
name = "handleSignInEvent",
filters = listOf(opcode(Opcode.INVOKE_VIRTUAL), opcode(Opcode.RETURN_VOID))
)

internal object MiniPlayerDefaultTextFingerprint : Fingerprint(
returnType = "V",
filters = listOf(
resourceLiteral(ResourceType.STRING, "mini_player_default_text")
)
)

internal object MiniPlayerDefaultViewVisibilityFingerprint : Fingerprint(
accessFlags = listOf(AccessFlags.PUBLIC, AccessFlags.FINAL),
returnType = "V",
parameters = listOf("Landroid/view/View;", "F"),
filters = listOf(
opcode(Opcode.IGET_OBJECT),
opcode(Opcode.SUB_FLOAT_2ADDR),
opcode(Opcode.SGET_OBJECT),
opcode(Opcode.INVOKE_VIRTUAL)
),
name = "a",
custom = { _, classDef -> classDef.methods.count() == 3 }
)
15 changes: 10 additions & 5 deletions patches/src/main/resources/addresources/values/music/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ Feature implementation by VazerOG"</string>
<!-- interaction.permanentrepeat.permanentRepeatPatch -->
<string name="morphe_music_play_permanent_repeat_title">Enable permanent repeat</string>

<!-- layout.buttons.hideButtons -->
<!-- layout.buttons.hideButtonsPatch -->
<string name="morphe_music_hide_cast_button_title">Hide cast button</string>
<string name="morphe_music_hide_history_button_title">Hide history button</string>
<string name="morphe_music_hide_notification_button_title">Hide notification button</string>
Expand All @@ -104,21 +104,26 @@ Feature implementation by VazerOG"</string>
<string name="morphe_change_start_page_entry_search">Search</string>
<string name="morphe_change_start_page_entry_subscriptions">Subscriptions</string>

<!-- layout.compactheader.hideCategoryBar -->
<!-- layout.compactheader.hideCategoryBarPatch -->
<string name="morphe_music_hide_category_bar_title">Hide category bar</string>

<!-- layout.miniplayer.changeMiniplayerColor -->
<!-- layout.miniplayer.changeMiniplayerColorPatch -->
<string name="morphe_music_change_miniplayer_color_title">Change miniplayer color</string>
<string name="morphe_music_change_miniplayer_color_summary">Matches the miniplayer color to the fullscreen player</string>

<!-- layout.miniplayer.miniplayerNextPreviousButtons -->
<!-- layout.miniplayer.miniplayerNextPreviousButtonsPatch -->
<string name="morphe_music_miniplayer_next_button_title">Show next button in miniplayer</string>
<string name="morphe_music_miniplayer_previous_button_title">Show previous button in miniplayer</string>

<!-- layout.miniplayer.enableForcedMiniplayer -->
<!-- layout.miniplayer.enableForcedMiniplayerPatch -->
<string name="morphe_music_enable_forced_miniplayer_title">Enable forced miniplayer</string>
<string name="morphe_music_enable_forced_miniplayer_summary">Prevents the player from automatically expanding to fullscreen when a new track starts</string>

<!-- layout.miniplayer.enableSwipeToDismissMiniplayerPatch -->
<string name="morphe_music_enable_swipe_to_dismiss_miniplayer_title">Enable swipe to dismiss miniplayer</string>
<string name="morphe_music_enable_swipe_to_dismiss_miniplayer_summary_on">Swiping down on miniplayer dismisses it</string>
<string name="morphe_music_enable_swipe_to_dismiss_miniplayer_summary_off">Swiping down on miniplayer does nothing</string>

<!-- layout.navigationbar.navigationBarPatch -->
<string name="morphe_music_navigation_bar_screen_title">Navigation bar</string>
<string name="morphe_music_navigation_bar_screen_summary">Choose which buttons appear in the navigation bar</string>
Expand Down
Loading