Fix Samsung unfolded long presses

This commit is contained in:
ajp_anton
2026-07-11 19:13:12 +00:00
parent 963dbd7b5f
commit 09bab5db90
6 changed files with 55 additions and 16 deletions
@@ -31,7 +31,7 @@ internal class ActionExecutor(
NavButtonAction.ASSISTANT -> sendConfiguredKeyPress(KeyEvent.KEYCODE_ASSIST) NavButtonAction.ASSISTANT -> sendConfiguredKeyPress(KeyEvent.KEYCODE_ASSIST)
NavButtonAction.RECENTS -> sendConfiguredKeyPress(KeyEvent.KEYCODE_APP_SWITCH) NavButtonAction.RECENTS -> sendConfiguredKeyPress(KeyEvent.KEYCODE_APP_SWITCH)
NavButtonAction.KILL_FOREGROUND_APP -> performPrivilegedActionOrDirect(PRIVILEGED_ACTION_KILL_FOREGROUND_APP) NavButtonAction.KILL_FOREGROUND_APP -> performPrivilegedActionOrDirect(PRIVILEGED_ACTION_KILL_FOREGROUND_APP)
NavButtonAction.TOGGLE_AUTO_ROTATE -> toggleAutoRotate() NavButtonAction.TOGGLE_AUTO_ROTATE -> performPrivilegedActionOrDirect(PRIVILEGED_ACTION_TOGGLE_AUTO_ROTATE)
NavButtonAction.TOGGLE_FLASHLIGHT -> performPrivilegedActionOrDirect(PRIVILEGED_ACTION_TOGGLE_FLASHLIGHT) NavButtonAction.TOGGLE_FLASHLIGHT -> performPrivilegedActionOrDirect(PRIVILEGED_ACTION_TOGGLE_FLASHLIGHT)
} }
} }
@@ -120,13 +120,14 @@ internal class ActionExecutor(
when (action) { when (action) {
PRIVILEGED_ACTION_KILL_FOREGROUND_APP -> killForegroundApp() PRIVILEGED_ACTION_KILL_FOREGROUND_APP -> killForegroundApp()
PRIVILEGED_ACTION_TOGGLE_FLASHLIGHT -> toggleFlashlight() PRIVILEGED_ACTION_TOGGLE_FLASHLIGHT -> toggleFlashlight()
PRIVILEGED_ACTION_TOGGLE_AUTO_ROTATE -> toggleAutoRotate()
} }
} else { } else {
requestPrivilegedAction(action, null) requestPrivilegedAction(action, null)
} }
} }
private fun toggleAutoRotate() { fun toggleAutoRotate() {
val context = contextProvider() ?: run { val context = contextProvider() ?: run {
log("Context unavailable, cannot toggle auto rotate.") log("Context unavailable, cannot toggle auto rotate.")
return return
@@ -72,6 +72,7 @@ internal const val EXTRA_PRIVILEGED_ACTION_TOKEN = "privileged_action_token"
internal const val EXTRA_PRIVILEGED_KEY_CODE = "privileged_key_code" internal const val EXTRA_PRIVILEGED_KEY_CODE = "privileged_key_code"
internal const val PRIVILEGED_ACTION_KILL_FOREGROUND_APP = "kill_foreground_app" internal const val PRIVILEGED_ACTION_KILL_FOREGROUND_APP = "kill_foreground_app"
internal const val PRIVILEGED_ACTION_TOGGLE_FLASHLIGHT = "toggle_flashlight" internal const val PRIVILEGED_ACTION_TOGGLE_FLASHLIGHT = "toggle_flashlight"
internal const val PRIVILEGED_ACTION_TOGGLE_AUTO_ROTATE = "toggle_auto_rotate"
internal const val PRIVILEGED_ACTION_SEND_KEY_PRESS = "send_key_press" internal const val PRIVILEGED_ACTION_SEND_KEY_PRESS = "send_key_press"
internal const val HONEYSPACE_NAV_BUTTON_VIEW = internal const val HONEYSPACE_NAV_BUTTON_VIEW =
@@ -68,6 +68,7 @@ class NavButtons() : XposedModule() {
}, },
killForegroundApp = actions::killForegroundApp, killForegroundApp = actions::killForegroundApp,
toggleFlashlight = actions::toggleFlashlight, toggleFlashlight = actions::toggleFlashlight,
toggleAutoRotate = actions::toggleAutoRotate,
sendKeyPress = actions::sendKeyPress, sendKeyPress = actions::sendKeyPress,
) )
private val sideArrowLongPresses: SideArrowLongPressController = SideArrowLongPressController( private val sideArrowLongPresses: SideArrowLongPressController = SideArrowLongPressController(
@@ -17,6 +17,7 @@ internal class PrivilegedActionBridge(
private val log: (String, Throwable?) -> Unit, private val log: (String, Throwable?) -> Unit,
private val killForegroundApp: () -> Unit, private val killForegroundApp: () -> Unit,
private val toggleFlashlight: () -> Unit, private val toggleFlashlight: () -> Unit,
private val toggleAutoRotate: () -> Unit,
private val sendKeyPress: (Int) -> Unit, private val sendKeyPress: (Int) -> Unit,
) { ) {
private var receiverRegistered = false private var receiverRegistered = false
@@ -78,6 +79,7 @@ internal class PrivilegedActionBridge(
when (action) { when (action) {
PRIVILEGED_ACTION_KILL_FOREGROUND_APP -> killForegroundApp() PRIVILEGED_ACTION_KILL_FOREGROUND_APP -> killForegroundApp()
PRIVILEGED_ACTION_TOGGLE_FLASHLIGHT -> toggleFlashlight() PRIVILEGED_ACTION_TOGGLE_FLASHLIGHT -> toggleFlashlight()
PRIVILEGED_ACTION_TOGGLE_AUTO_ROTATE -> toggleAutoRotate()
PRIVILEGED_ACTION_SEND_KEY_PRESS -> { PRIVILEGED_ACTION_SEND_KEY_PRESS -> {
if (keyCode != KeyEvent.KEYCODE_UNKNOWN) { if (keyCode != KeyEvent.KEYCODE_UNKNOWN) {
sendKeyPress(keyCode) sendKeyPress(keyCode)
@@ -92,12 +94,12 @@ internal class PrivilegedActionBridge(
senderPackage: String?, senderPackage: String?,
senderUid: Int, senderUid: Int,
): Boolean { ): Boolean {
if (senderPackage == SYSTEMUI_PACKAGE || senderPackage in AOSP_LAUNCHER_PACKAGES) { if (isTrustedUiPackage(senderPackage)) {
return true return true
} }
val caller = getParcelableExtraCompat(intent, EXTRA_PRIVILEGED_CALLER, PendingIntent::class.java) val caller = getParcelableExtraCompat(intent, EXTRA_PRIVILEGED_CALLER, PendingIntent::class.java)
if (caller?.creatorPackage == SYSTEMUI_PACKAGE || caller?.creatorPackage in AOSP_LAUNCHER_PACKAGES) { if (isTrustedUiPackage(caller?.creatorPackage)) {
return true return true
} }
if (caller?.creatorUid == Process.SYSTEM_UID) { if (caller?.creatorUid == Process.SYSTEM_UID) {
@@ -117,7 +119,13 @@ internal class PrivilegedActionBridge(
return true return true
} }
return context.packageManager.getPackagesForUid(senderUid) return context.packageManager.getPackagesForUid(senderUid)
?.any { it == SYSTEMUI_PACKAGE || it in AOSP_LAUNCHER_PACKAGES } == true ?.any(::isTrustedUiPackage) == true
}
private fun isTrustedUiPackage(packageName: String?): Boolean {
return packageName == SYSTEMUI_PACKAGE ||
packageName == SAMSUNG_LAUNCHER_PACKAGE ||
packageName in AOSP_LAUNCHER_PACKAGES
} }
private fun readToken(): String? { private fun readToken(): String? {
@@ -3,6 +3,7 @@ package se.ajpanton.navbuttons
import android.content.Context import android.content.Context
import android.os.Handler import android.os.Handler
import android.os.SystemClock import android.os.SystemClock
import android.view.HapticFeedbackConstants
import android.view.KeyEvent import android.view.KeyEvent
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View import android.view.View
@@ -28,6 +29,7 @@ internal class SamsungHoneyspaceHooks(
private val log: (String) -> Unit, private val log: (String) -> Unit,
) { ) {
private val longPressTriggered = WeakHashMap<Any, Boolean>() private val longPressTriggered = WeakHashMap<Any, Boolean>()
private val customLongPressRunnables = WeakHashMap<Any, Runnable>()
private val trackedNavContainers = Collections.newSetFromMap(WeakHashMap<Any, Boolean>()) private val trackedNavContainers = Collections.newSetFromMap(WeakHashMap<Any, Boolean>())
private val nativeSideArrowStates = WeakHashMap<Any, Boolean>() private val nativeSideArrowStates = WeakHashMap<Any, Boolean>()
private val homeStockLongPressGuard = HomeStockLongPressGuard( private val homeStockLongPressGuard = HomeStockLongPressGuard(
@@ -86,20 +88,26 @@ internal class SamsungHoneyspaceHooks(
when (motionEvent.actionMasked) { when (motionEvent.actionMasked) {
MotionEvent.ACTION_DOWN -> { MotionEvent.ACTION_DOWN -> {
longPressTriggered[navButtonView] = false longPressTriggered[navButtonView] = false
cancelCustomLongPress(navButtonView)
homeStockLongPressGuard.cancel(navButtonView) homeStockLongPressGuard.cancel(navButtonView)
homeStockLongPressGuard.clear(navButtonView) homeStockLongPressGuard.clear(navButtonView)
val result = chain.proceed() val result = chain.proceed()
rescheduleHoneyspaceLongPress( val config = configProvider(buttonId)
if (config.longPressAction != NavButtonAction.STOCK) {
cancelHoneyspaceLongPress(navButtonView, runnableClass)
scheduleCustomLongPress(
navButtonView, navButtonView,
runnableClass, config.longPressAction,
configProvider(buttonId).longPressDurationMs, config.longPressDurationMs,
) )
}
homeStockLongPressGuard.schedule(navButtonView, buttonId) homeStockLongPressGuard.schedule(navButtonView, buttonId)
result result
} }
MotionEvent.ACTION_UP -> { MotionEvent.ACTION_UP -> {
val triggered = longPressTriggered[navButtonView] == true val triggered = longPressTriggered[navButtonView] == true
cancelCustomLongPress(navButtonView)
if (triggered) { if (triggered) {
return@hookMethod proceedWithCanceledAction(motionEvent) { return@hookMethod proceedWithCanceledAction(motionEvent) {
chain.proceed() chain.proceed()
@@ -137,6 +145,7 @@ internal class SamsungHoneyspaceHooks(
} }
MotionEvent.ACTION_CANCEL -> { MotionEvent.ACTION_CANCEL -> {
cancelCustomLongPress(navButtonView)
homeStockLongPressGuard.cancel(navButtonView) homeStockLongPressGuard.cancel(navButtonView)
longPressTriggered.remove(navButtonView) longPressTriggered.remove(navButtonView)
homeStockLongPressGuard.clear(navButtonView) homeStockLongPressGuard.clear(navButtonView)
@@ -179,6 +188,30 @@ internal class SamsungHoneyspaceHooks(
return true return true
} }
private fun scheduleCustomLongPress(
navButtonView: Any,
action: NavButtonAction,
durationMs: Int,
) {
val view = navButtonView as? View ?: return
val runnable = Runnable {
updateContextFromView(navButtonView)
longPressTriggered[navButtonView] = true
markLongClicked(navButtonView)
homeStockLongPressGuard.cancel(navButtonView)
view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
performAction(action)
}
customLongPressRunnables[navButtonView] = runnable
view.postDelayed(runnable, durationMs.toLong())
}
private fun cancelCustomLongPress(navButtonView: Any) {
customLongPressRunnables.remove(navButtonView)?.let { runnable ->
(navButtonView as? View)?.removeCallbacks(runnable)
}
}
fun refreshSideArrowLayouts(reason: String) { fun refreshSideArrowLayouts(reason: String) {
val containers = trackedNavContainers.toList() val containers = trackedNavContainers.toList()
containers.forEach { container -> containers.forEach { container ->
@@ -48,15 +48,10 @@ internal fun isHoneyspaceNavButtonContainer(view: View): Boolean {
honeyspaceViewIdName(view) == "navbar_button_container" honeyspaceViewIdName(view) == "navbar_button_container"
} }
internal fun rescheduleHoneyspaceLongPress( internal fun cancelHoneyspaceLongPress(navButtonView: Any, runnableClass: Class<*>) {
navButtonView: Any,
runnableClass: Class<*>,
durationMs: Int,
) {
val view = navButtonView as? View ?: return val view = navButtonView as? View ?: return
val longPressRunnable = findHoneyspaceLongPressRunnable(navButtonView, runnableClass) ?: return val longPressRunnable = findHoneyspaceLongPressRunnable(navButtonView, runnableClass) ?: return
view.removeCallbacks(longPressRunnable) view.removeCallbacks(longPressRunnable)
view.postDelayed(longPressRunnable, durationMs.toLong())
} }
internal fun createHoneyspaceExtraButton( internal fun createHoneyspaceExtraButton(