From c22cbc9f43bbf0bc01e9feb4ffce174442017035 Mon Sep 17 00:00:00 2001 From: The412Banner Date: Sat, 19 Sep 2026 13:49:29 -0400 Subject: [PATCH] Type into the session: hardware keys to the compositor, and a log path that always exists MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The session script redirects its own output into BL_LOG with exec, and a redirection a non-interactive shell cannot open ends that shell — so a denied storage permission would have killed the session rather than just losing the log. Downloads is used once proven writable, files/logs otherwise. Keyboard events now reach wl_keyboard, which is what the client's first-run login needs on a device with a keyboard attached. --- .../the412banner/steamdeck/SessionActivity.kt | 44 +++++++++++- .../steamdeck/input/EvdevKeys.java | 72 +++++++++++++++++++ app/src/main/res/values/strings.xml | 2 +- 3 files changed, 115 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/com/the412banner/steamdeck/input/EvdevKeys.java diff --git a/app/src/main/java/com/the412banner/steamdeck/SessionActivity.kt b/app/src/main/java/com/the412banner/steamdeck/SessionActivity.kt index 83eb9626b..b7cd4bdc3 100644 --- a/app/src/main/java/com/the412banner/steamdeck/SessionActivity.kt +++ b/app/src/main/java/com/the412banner/steamdeck/SessionActivity.kt @@ -22,6 +22,7 @@ import com.the412banner.steamdeck.core.EnvironmentComponent import com.the412banner.steamdeck.core.FileUtils import com.the412banner.steamdeck.core.ProcessHelper import com.the412banner.steamdeck.gpu.TurnipDriver +import com.the412banner.steamdeck.input.EvdevKeys import com.the412banner.steamdeck.input.FakeInputWriter import com.the412banner.steamdeck.input.PadBridge import com.the412banner.steamdeck.runtime.LinuxNetworkLinkComponent @@ -173,7 +174,7 @@ class SessionActivity : Activity(), SurfaceHolder.Callback { val sessionRoot = LinuxRuntime.sessionRoot(this).apply { mkdirs() } stageSessionFiles(root) - val logDir = LinuxRuntime.debugLogDir().apply { mkdirs() } + val logDir = logDirectory() val stamp = SimpleDateFormat("yyyyMMdd-HHmmss", Locale.US).format(Date()) sessionLog = File(logDir, "session-$stamp.log") @@ -273,6 +274,29 @@ class SessionActivity : Activity(), SurfaceHolder.Callback { Log.i(TAG, "session pid $sessionPid, log ${sessionLog.path}") } + /** + * Where the session writes its log. Downloads is the point — a failed run is handed over as a + * folder rather than dug out of app-private storage — but the session script redirects its own + * output there with `exec`, and a redirection a non-interactive shell cannot open ends that + * shell. So a public directory is used only once it is proven writable; otherwise the app's + * own files directory, which is bound into the session anyway, stands in. + */ + private fun logDirectory(): File { + val public = LinuxRuntime.debugLogDir() + if (public.isDirectory || public.mkdirs()) { + val probe = File(public, ".writable") + try { + if (probe.createNewFile() || probe.isFile) { + probe.delete() + return public + } + } catch (ignored: Exception) { + } + } + Log.w(TAG, "$public is not writable (storage permission?); logging to files/logs") + return File(filesDir, "logs").apply { mkdirs() } + } + /** * The libraries and scripts the session runs, refreshed from the apk at every launch. * @@ -361,12 +385,28 @@ class SessionActivity : Activity(), SurfaceHolder.Callback { } private fun sessionLogName(): String = - if (::sessionLog.isInitialized) "${LinuxRuntime.DEBUG_LOG_DIR}/${sessionLog.name}" else "-" + if (::sessionLog.isInitialized) sessionLog.path else "-" // ── Input ─────────────────────────────────────────────────────────────────────────────── override fun dispatchKeyEvent(event: KeyEvent): Boolean { if (padBridge?.onKeyEvent(event) == true) return true + // A hardware keyboard, forwarded to the compositor's wl_keyboard. This is how an account + // name and password get typed on the client's first run; the client's own on-screen + // keyboard covers a device without one, driven by touch or the pad. + if (compositorStarted && event.device != null && !PadBridge.isFromController(event.device)) { + val down = event.action == KeyEvent.ACTION_DOWN + if (down || event.action == KeyEvent.ACTION_UP) { + // An unmapped key may still be a real one on a foreign layout: its scan code is + // the evdev code the kernel gave Android in the first place. + var evdev = EvdevKeys.fromKeyCode(event.keyCode) + if (evdev < 0 && event.scanCode > 0) evdev = event.scanCode + if (evdev > 0) { + WaylandCompositor.nativeSendKey(evdev, if (down) 1 else 0) + return true + } + } + } return super.dispatchKeyEvent(event) } diff --git a/app/src/main/java/com/the412banner/steamdeck/input/EvdevKeys.java b/app/src/main/java/com/the412banner/steamdeck/input/EvdevKeys.java new file mode 100644 index 000000000..a5b1e3775 --- /dev/null +++ b/app/src/main/java/com/the412banner/steamdeck/input/EvdevKeys.java @@ -0,0 +1,72 @@ +package com.the412banner.steamdeck.input; + +import android.view.KeyEvent; + +/** + * Android key codes to Linux evdev codes, for the keys we inject into the compositor's + * wl_keyboard. A hardware keyboard is how an account name and password are typed on the client's + * first run, so this matters before any game does. + * + *

evdev's order is not alphabetical: the numbers are the physical positions of a PC keyboard, + * which is what the compositor's xkb keymap is written against. Table taken from Bannerlator's + * Wayland path unchanged. + */ +public final class EvdevKeys { + private EvdevKeys() {} + + public static int fromKeyCode(int kc) { + switch (kc) { + // Letters (evdev order is NOT alphabetical) + case KeyEvent.KEYCODE_A: return 30; case KeyEvent.KEYCODE_B: return 48; + case KeyEvent.KEYCODE_C: return 46; case KeyEvent.KEYCODE_D: return 32; + case KeyEvent.KEYCODE_E: return 18; case KeyEvent.KEYCODE_F: return 33; + case KeyEvent.KEYCODE_G: return 34; case KeyEvent.KEYCODE_H: return 35; + case KeyEvent.KEYCODE_I: return 23; case KeyEvent.KEYCODE_J: return 36; + case KeyEvent.KEYCODE_K: return 37; case KeyEvent.KEYCODE_L: return 38; + case KeyEvent.KEYCODE_M: return 50; case KeyEvent.KEYCODE_N: return 49; + case KeyEvent.KEYCODE_O: return 24; case KeyEvent.KEYCODE_P: return 25; + case KeyEvent.KEYCODE_Q: return 16; case KeyEvent.KEYCODE_R: return 19; + case KeyEvent.KEYCODE_S: return 31; case KeyEvent.KEYCODE_T: return 20; + case KeyEvent.KEYCODE_U: return 22; case KeyEvent.KEYCODE_V: return 47; + case KeyEvent.KEYCODE_W: return 17; case KeyEvent.KEYCODE_X: return 45; + case KeyEvent.KEYCODE_Y: return 21; case KeyEvent.KEYCODE_Z: return 44; + // Digit row + case KeyEvent.KEYCODE_1: return 2; case KeyEvent.KEYCODE_2: return 3; + case KeyEvent.KEYCODE_3: return 4; case KeyEvent.KEYCODE_4: return 5; + case KeyEvent.KEYCODE_5: return 6; case KeyEvent.KEYCODE_6: return 7; + case KeyEvent.KEYCODE_7: return 8; case KeyEvent.KEYCODE_8: return 9; + case KeyEvent.KEYCODE_9: return 10; case KeyEvent.KEYCODE_0: return 11; + // Whitespace / edit + case KeyEvent.KEYCODE_ENTER: return 28; case KeyEvent.KEYCODE_NUMPAD_ENTER: return 28; + case KeyEvent.KEYCODE_SPACE: return 57; case KeyEvent.KEYCODE_TAB: return 15; + case KeyEvent.KEYCODE_DEL: return 14; /* backspace */ + case KeyEvent.KEYCODE_FORWARD_DEL: return 111; case KeyEvent.KEYCODE_ESCAPE: return 1; + // Modifiers + case KeyEvent.KEYCODE_SHIFT_LEFT: return 42; case KeyEvent.KEYCODE_SHIFT_RIGHT: return 54; + case KeyEvent.KEYCODE_CTRL_LEFT: return 29; case KeyEvent.KEYCODE_CTRL_RIGHT: return 97; + case KeyEvent.KEYCODE_ALT_LEFT: return 56; case KeyEvent.KEYCODE_ALT_RIGHT: return 100; + case KeyEvent.KEYCODE_CAPS_LOCK: return 58; + // Arrows / nav + case KeyEvent.KEYCODE_DPAD_UP: return 103; case KeyEvent.KEYCODE_DPAD_DOWN: return 108; + case KeyEvent.KEYCODE_DPAD_LEFT: return 105; case KeyEvent.KEYCODE_DPAD_RIGHT: return 106; + case KeyEvent.KEYCODE_MOVE_HOME: return 102; case KeyEvent.KEYCODE_MOVE_END: return 107; + case KeyEvent.KEYCODE_PAGE_UP: return 104; case KeyEvent.KEYCODE_PAGE_DOWN: return 109; + case KeyEvent.KEYCODE_INSERT: return 110; + // Punctuation + case KeyEvent.KEYCODE_GRAVE: return 41; case KeyEvent.KEYCODE_MINUS: return 12; + case KeyEvent.KEYCODE_EQUALS: return 13; case KeyEvent.KEYCODE_LEFT_BRACKET: return 26; + case KeyEvent.KEYCODE_RIGHT_BRACKET: return 27; case KeyEvent.KEYCODE_BACKSLASH: return 43; + case KeyEvent.KEYCODE_SEMICOLON: return 39; case KeyEvent.KEYCODE_APOSTROPHE: return 40; + case KeyEvent.KEYCODE_SLASH: return 53; case KeyEvent.KEYCODE_COMMA: return 51; + case KeyEvent.KEYCODE_PERIOD: return 52; + // Function row + case KeyEvent.KEYCODE_F1: return 59; case KeyEvent.KEYCODE_F2: return 60; + case KeyEvent.KEYCODE_F3: return 61; case KeyEvent.KEYCODE_F4: return 62; + case KeyEvent.KEYCODE_F5: return 63; case KeyEvent.KEYCODE_F6: return 64; + case KeyEvent.KEYCODE_F7: return 65; case KeyEvent.KEYCODE_F8: return 66; + case KeyEvent.KEYCODE_F9: return 67; case KeyEvent.KEYCODE_F10: return 68; + case KeyEvent.KEYCODE_F11: return 87; case KeyEvent.KEYCODE_F12: return 88; + default: return -1; + } + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b2b0638c3..538f28011 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -17,5 +17,5 @@ Could not reach the runtime catalog Install failed — nothing was changed Starting the session… - The session ended (%1$d). Log: Download/%2$s + The session ended (%1$d)\n%2$s