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 @@