Compare commits

...
5 changed files with 202 additions and 18 deletions

No files matched your search

@@ -65,6 +65,7 @@ import com.droiddeck.launcher.input.SessionClipboard
import com.droiddeck.launcher.input.OnScreenControls
import com.droiddeck.launcher.input.PadBridge
import com.droiddeck.launcher.input.SteamControllerBle
import com.droiddeck.launcher.input.DirectTouchHold
import com.droiddeck.launcher.input.PointerGestures
import com.droiddeck.launcher.input.SecondScreenDisplay
import com.droiddeck.launcher.input.SecondScreenDisplays
@@ -162,6 +163,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
}
private var watching = true
private lateinit var touchpad: TouchpadGestures
private lateinit var directTouchHold: DirectTouchHold
private var touchMode by mutableStateOf(SessionPrefs.TOUCH_AUTO)
private var cursorPos by mutableStateOf(androidx.compose.ui.geometry.Offset(-100f, -100f))
private var cursorVisible by mutableStateOf(false)
@@ -397,6 +399,9 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
onScreenControls = OnScreenControls(this, bridge, onKeyboard = ::togglePcKeyboard).also { root.addView(it) }
keyboard = KeyboardHost(this).also { root.addView(it) }
touchpad = TouchpadGestures(PointerGestures.slop(this), pointerListener)
directTouchHold = DirectTouchHold(PointerGestures.slop(this), pointerListener) {
WaylandCompositor.nativeSendTouch(3, -1, 0, 0)
}
val noCursor = android.view.PointerIcon.getSystemIcon(this, android.view.PointerIcon.TYPE_NULL)
root.pointerIcon = noCursor
surfaceView.pointerIcon = noCursor
@@ -1065,6 +1070,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
val nextTouchMode = SessionPrefs.touchMode(this)
if (touchMode != nextTouchMode) {
if (::touchpad.isInitialized) touchpad.cancel()
if (::directTouchHold.isInitialized) directTouchHold.cancel()
if (CompositorHost.isStarted) {
WaylandCompositor.nativeSendTouch(3, -1, 0, 0)
listOf(PointerGestures.BTN_LEFT, PointerGestures.BTN_RIGHT, PointerGestures.BTN_MIDDLE).forEach {
@@ -1927,6 +1933,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
}
if (next == routedTouchMode) return
if (::touchpad.isInitialized) touchpad.cancel()
if (::directTouchHold.isInitialized) directTouchHold.cancel()
if (CompositorHost.isStarted) WaylandCompositor.nativeSendTouch(3, -1, 0, 0)
if (next == SessionPrefs.TOUCH_OFF) cursorVisible = false
routedTouchMode = next
@@ -1963,6 +1970,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
return touchpad.onTouch(event)
}
val rect = drawnRect() ?: return false
if (directTouchHold.onTouch(event)) return true
fun sendTouch(action: Int, index: Int) {
val x = event.getX(index).coerceIn(rect.left, rect.right)
val y = event.getY(index).coerceIn(rect.top, rect.bottom)
@@ -2101,6 +2109,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
controls.visibility = View.GONE
steamTouchControls?.let { it.releaseAll(); it.visibility = View.GONE }
com.droiddeck.launcher.input.SteamTouchDevice.current?.plugged = false
applyTouchRouting()
return
}
val buttonsOnly = forced == SessionPrefs.OSC_STEAM_QAM
@@ -2113,6 +2122,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
if (steam == null) {
steamTouchControls?.let { it.releaseAll(); it.visibility = View.GONE }
com.droiddeck.launcher.input.SteamTouchDevice.current?.plugged = false
applyTouchRouting()
}
controls.setButtonsOnly(buttonsOnly)
val show = when (forced) {
@@ -2174,6 +2184,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
onScreenControls?.releaseAll()
padBridge?.releaseAll()
touchpad.cancel()
directTouchHold.cancel()
guestKeysDown.toList().forEach { sendGuestKey(it, false) }
keyboard?.hide()
sessionClipboard.stop()
@@ -2287,6 +2298,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
override fun onPause() {
if (runtimeRemovalBlocked) { super.onPause(); return }
sessionClipboard.stop()
if (::directTouchHold.isInitialized) directTouchHold.cancel()
drawerOpen = false
pcKeyboardOpen = false
achievementsOpen = false
@@ -0,0 +1,53 @@
package com.droiddeck.launcher.input
import android.os.Handler
import android.os.Looper
import android.view.MotionEvent
import kotlin.math.abs
class DirectTouchHold(
private val slopPx: Float,
private val listener: PointerGestures.Listener,
private val cancelTouch: () -> Unit,
) {
private val handler = Handler(Looper.getMainLooper())
private var x = 0f
private var y = 0f
private var consumed = false
private var pressed = false
private val release = Runnable {
if (pressed) listener.onButton(PointerGestures.BTN_RIGHT, false, x, y)
pressed = false
}
private val hold = Runnable {
cancelTouch()
consumed = true
listener.onLongPress()
listener.onButton(PointerGestures.BTN_RIGHT, true, x, y)
pressed = true
handler.postDelayed(release, 50L)
}
fun cancel() {
handler.removeCallbacks(hold)
handler.removeCallbacks(release)
release.run()
consumed = false
}
fun onTouch(event: MotionEvent): Boolean {
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
cancel()
x = event.x; y = event.y
handler.postDelayed(hold, 500L)
}
MotionEvent.ACTION_MOVE -> if (event.pointerCount > 1 ||
abs(event.x - x) > slopPx || abs(event.y - y) > slopPx) handler.removeCallbacks(hold)
MotionEvent.ACTION_POINTER_DOWN, MotionEvent.ACTION_POINTER_UP,
MotionEvent.ACTION_UP -> handler.removeCallbacks(hold)
MotionEvent.ACTION_CANCEL -> cancel()
}
return consumed
}
}
@@ -0,0 +1,66 @@
package com.droiddeck.launcher.input
import android.os.Looper
import android.view.MotionEvent
import java.time.Duration
import org.junit.Assert.*
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.Shadows.shadowOf
import org.robolectric.annotation.Config
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [28], manifest = Config.NONE)
class DirectTouchHoldTest {
private val events = mutableListOf<String>()
private val hold = DirectTouchHold(8f, object : PointerGestures.Listener {
override fun onMove(x: Float, y: Float) {}
override fun onButton(button: Int, pressed: Boolean, x: Float, y: Float) {
assertEquals(PointerGestures.BTN_RIGHT, button)
events += "$pressed at $x,$y"
}
override fun onWheel(steps: Int) {}
override fun onLongPress() {}
}) { events += "cancel touch" }
@Test fun holdReleasesRawTouchBeforeClickAndConsumesTheLift() {
assertFalse(touch(MotionEvent.ACTION_DOWN))
advance(500)
assertEquals(listOf("cancel touch", "true at 40.0,30.0"), events)
assertTrue(touch(MotionEvent.ACTION_UP))
advance(50)
assertEquals("false at 40.0,30.0", events.last())
assertFalse(touch(MotionEvent.ACTION_DOWN))
hold.cancel()
}
@Test fun tapDragSecondFingerAndCancellationDoNotRightClick() {
for (action in listOf(MotionEvent.ACTION_UP, MotionEvent.ACTION_MOVE,
MotionEvent.ACTION_POINTER_DOWN, MotionEvent.ACTION_CANCEL)) {
touch(MotionEvent.ACTION_DOWN)
assertFalse(touch(action, 60f))
advance(600)
assertTrue(events.isEmpty())
hold.cancel()
}
}
@Test fun modeChangeCancelsPendingHoldAndReleasesAnActiveClick() {
touch(MotionEvent.ACTION_DOWN)
hold.cancel()
advance(600)
assertTrue(events.isEmpty())
touch(MotionEvent.ACTION_DOWN)
advance(500)
hold.cancel()
advance(100)
assertEquals(listOf("cancel touch", "true at 40.0,30.0", "false at 40.0,30.0"), events)
}
private fun advance(ms: Long) = shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(ms))
private fun touch(action: Int, x: Float = 40f): Boolean {
val event = MotionEvent.obtain(0, 0, action, x, 30f, 0)
return hold.onTouch(event).also { event.recycle() }
}
}
@@ -0,0 +1,69 @@
package com.droiddeck.launcher.input
import android.view.View
import com.droiddeck.launcher.SessionActivity
import com.droiddeck.launcher.session.SessionPrefs
import com.droiddeck.launcher.session.SessionService
import com.droiddeck.launcher.session.SessionState
import java.nio.file.Files
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.Robolectric
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import org.robolectric.util.ReflectionHelpers
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [28], manifest = Config.NONE)
class SessionTouchRoutingTest {
private lateinit var activity: SessionActivity
private lateinit var steam: SteamTouchControls
private lateinit var touchpad: TouchpadGestures
private val directory = Files.createTempDirectory("touch-routing").toFile()
@Before fun setUp() {
activity = Robolectric.buildActivity(SessionActivity::class.java).get()
activity.getSharedPreferences("session", 0).edit().clear().commit()
SessionState.mode = SessionService.MODE_STEAM
SteamTouchDevice.prepare(directory)
steam = SteamTouchControls(activity, SteamTouchDevice.current!!, {}, {})
steam.visibility = View.VISIBLE
touchpad = TouchpadGestures(8f, object : PointerGestures.Listener {
override fun onMove(x: Float, y: Float) {}
override fun onButton(button: Int, pressed: Boolean, x: Float, y: Float) {}
override fun onWheel(steps: Int) {}
override fun onLongPress() {}
})
field("onScreenControls", OnScreenControls(activity, null).apply { visibility = View.GONE })
field("steamTouchControls", steam)
field("steamTouchOptions", SteamTouchConfig.Options(inputMode = SteamTouchConfig.INPUT_MOUSE, mouseMode = SteamTouchConfig.MOUSE_ABSOLUTE, trackpadSensitivity = 2f))
field("touchpad", touchpad)
field("routedTouchMode", SessionPrefs.TOUCH_DIRECT)
}
@After fun tearDown() {
SteamTouchDevice.release()
directory.deleteRecursively()
SessionState.mode = SessionService.MODE_STEAM
}
@Test fun disablingSteamTouchRestoresEverySavedModeEvenWhenRegularControlsAreAlreadyHidden() {
for (mode in listOf(SessionPrefs.TOUCH_PAD, SessionPrefs.TOUCH_DIRECT, SessionPrefs.TOUCH_AUTO, SessionPrefs.TOUCH_OFF)) {
SessionPrefs.setTouchMode(activity, mode)
SessionPrefs.setOscMode(activity, SessionPrefs.OSC_NEVER)
steam.visibility = View.VISIBLE
invoke("readPrefs")
assertEquals(SessionPrefs.TOUCH_DIRECT, ReflectionHelpers.getField<String>(activity, "routedTouchMode"))
invoke("updateOnScreenControls")
assertEquals(View.GONE, steam.visibility)
assertEquals(mode, ReflectionHelpers.getField<String>(activity, "routedTouchMode"))
assertEquals(1.15f, touchpad.sensitivity, 0f)
}
}
private fun field(name: String, value: Any) = ReflectionHelpers.setField(activity, name, value)
private fun invoke(name: String) = ReflectionHelpers.callInstanceMethod<Any?>(activity, name)
}
@@ -11,20 +11,8 @@ diff --git a/src/syscall/seccomp.c b/src/syscall/seccomp.c
index ee7b8acd68660f38fd52f56e951ffb94e9061d68..ff3a79b60e9092a296bb444c919bb3861338727f 100644
--- a/src/syscall/seccomp.c
+++ b/src/syscall/seccomp.c
@@ -738,4 +738,4 @@ int enable_syscall_filtering(const Tracee *tracee)
- * the statx one emulates the call when it fails, which only a kernel
- * before 4.11 does for want of it - a statx Android's policy traps is
- * emulated from SIGSYS instead. Both stops re-read and re-translate
- * the path, and are paid on every call.
+ * the statx one emulates the syscall before 4.11 and supplies MNT_ID
+ * from fdinfo through 5.7. Android policy traps are still emulated from
+ * SIGSYS instead. Both stops re-read and re-translate the path, and are
+ * paid on every call.
@@ -764 +764,4 @@ static void drop_unneeded_exit_stops(void)
@@ -764 +764 @@ static void drop_unneeded_exit_stops(void)
- if (major > 4 || (major == 4 && minor >= 11))
+ /* Linux 4.11 introduced statx; Linux 5.8 added MNT_ID.
+ * Keep its exit stop through 5.7 so the missing ID can be
+ * recovered from the descriptor's fdinfo. */
+ if (major > 5 || (major == 5 && minor >= 8))
diff --git a/src/tracee/statx.c b/src/tracee/statx.c
index ca972f892e84461cb694dd13a37bfbc67527b320..824771ccf5934e1f870fc36a29fb22fdcad0a856 100644
@@ -32,11 +20,7 @@ index ca972f892e84461cb694dd13a37bfbc67527b320..824771ccf5934e1f870fc36a29fb22fd
+++ b/src/tracee/statx.c
@@ -4,0 +5 @@
+#include "tracee/statx_mnt_id.h"
@@ -125,0 +127,9 @@ int handle_statx_syscall(Tracee *tracee, bool from_sigsys) {
+ /* libglnx chases an already-open fd with AT_EMPTY_PATH and requires a
+ * mount ID. Kernels before 5.8 can succeed without returning that field.
+ * The tracee is stopped at this syscall exit; accept only the ordinary
+ * mount ID and verify the fdinfo inode against the statx result. */
@@ -125,0 +127,5 @@ int handle_statx_syscall(Tracee *tracee, bool from_sigsys) {
+ if (droiddeck_proot_statx_fill_mnt_id(tracee->pid, (int) dirfd,
+ (int) flags, guest_path, (unsigned int) mask,
+ &state.statx_buf))