Skip to content

Latest commit

 

History

History
157 lines (122 loc) · 7.25 KB

File metadata and controls

157 lines (122 loc) · 7.25 KB

sshx mobile — Android app

A thin, full-screen WebView wrapper around an sshx.io session that makes it usable on a phone. It loads the session URL, injects the validated in-page overlay (../sshx-mobile.js, shipped as an asset), and — the reason this is a native app and not a bookmarklet — drives a trusted resize-handle drag via WebView.dispatchTouchEvent(MotionEvent) so the sshx PTY reflows crisply to the device viewport. It can also scan a session QR with CameraX + ML Kit.

  • Package / applicationId: dev.moukrea.sshxmobile
  • App label: sshx mobile
  • Min / target / compile SDK: 26 / 35 / 35
  • The authoritative build contract is SPEC.md.

You need the Android SDK to build this. This repo machine has the JDK but no Gradle and no Android SDK, so the app is built in Android Studio, not here. See Limitations below.


Build & install (Android Studio)

  1. Install Android Studio (Ladybug / 2024.2.1 or newer) with:

    • Android SDK Platform 35,
    • Android SDK Build-Tools 35.x,
    • a device or emulator running Android 8.0 (API 26) or newer. The pinned toolchain (AGP 8.7.3, Gradle 8.11.1, Kotlin 2.0.21, Compose BOM 2024.12.01, CameraX 1.4.1, ML Kit barcode 17.3.0) is declared in the Gradle files; Android Studio downloads it automatically. Build with JDK 17jvmToolchain(17) is set, so a host JDK 21 is fine.
  2. Open the project: File → Open → select the android/ directory (the one containing settings.gradle.kts). Let Gradle sync finish (first sync downloads the SDK/AGP/Compose artifacts).

  3. Run / install:

    • Plug in a device with USB debugging on, or start an emulator.
    • Press Run ▶ (the app configuration), or from a terminal that has the Android SDK on its PATH:
      cd android
      ./gradlew installDebug        # installs the debug APK on the connected device
      # or build an APK to sideload:
      ./gradlew assembleDebug       # -> app/build/outputs/apk/debug/app-debug.apk
  4. Grant permissions on first launch: Camera (only when you open the QR scanner) and Internet (granted at install).


Usage

On launch you get the entry screen:

  • Enter a room id / URL. Paste any of these into the text field and tap Connect:

    • a full URL: https://sshx.io/s/UuWX4sOSrm#dmGcdlAyGfkrMf
    • a tail: s/UuWX4sOSrm#dmGcdlAyGfkrMf
    • a slashed tail: /s/UuWX4sOSrm#dmGcdlAyGfkrMf

    The input is run through UrlNormalizer → canonical https://sshx.io/s/<id>#<fragment>. The #fragment is the end-to-end decryption key and is passed verbatim to the WebView — never stripped, re-encoded, or sent to the server. The last successful URL is remembered and pre-filled next launch.

  • Scan QR. Opens the camera scanner. Point it at a QR printed by the host tool ../host/sshx-qr. The first decoded value (the full URL) is normalized and the session opens automatically.

  • Resolver URL (optional). A stable address (e.g. a Tailscale http://<host>.<tailnet>.ts.net:8765/current-url) served by the host's optional resolver. When set, the app fetches the current session URL from it instead of you re-scanning after a host restart. See Persistence below.

In the terminal (overlay)

Once the session loads, the injected overlay hides sshx's desktop chrome and gives you a phone layout: a tab bar (close / new-tab / zoom) on top, the active terminal filling the height, and a keyboard-aware ← ↑ ↓ → bar at the bottom that lifts above the on-screen keyboard.


How reflow works (why it's a native app)

sshx ignores synthetic JS pointer/wheel events, so injected JavaScript cannot resize/reflow the PTY. But a real/trusted drag of a terminal's bottom-right resize handle does reflow the PTY crisply (more rows/cols, native font). In an Android WebView, JS injected via evaluateJavascript is still synthetic — but native WebView.dispatchTouchEvent(MotionEvent) delivers trusted touch.

So ReflowController performs, on the main thread, for each terminal tab:

  1. prepReflow(i) (overlay JS) → switch to tab i, set mode = "native" + zoom 1 so the handle sits at a real, unscaled, draggable coordinate; it returns the handle position and the bottom-right target, both in CSS px.
  2. performTrustedDrag(...) → converts CSS px to device px (devicePx = cssPx * displayMetrics.density) and dispatches a fixed-downTime ACTION_DOWN → interpolated ACTION_MOVEs → ACTION_UP gesture, dragging the handle to the target. This is the trusted touch that makes sshx reflow.
  3. After all tabs, finishReflow("fillHeight") returns to the height-filling display mode (now near 1:1, so text stays crisp) and restores the previously active tab.

The sweep re-runs (debounced, coalesced — only one at a time, latest viewport wins) on: overlay-ready, layout changes (rotation / multi-window / foldable / keyboard via addOnLayoutChangeListener), the keyboard inset-animation end fast path, onConfigurationChanged, and the overlay's own visualViewport change callback. A fallback poll covers a missed bridge call.


Persistence & the resolver (optional)

sshx's reconnect loop survives network changes without changing the URL, so a phone holding the URL just waits through outages. But a new URL is minted on every host-process restart (there is no session-id pinning flag).

To avoid re-scanning after a restart, the host can run a tiny resolver that always serves the current URL at a stable address. Set that address in the app's Resolver URL field; the app fetches it (running the response through the same normalizer) on connect, and re-fetches on a WebView connection error or a manual refresh.

Because the resolver returns the URL — including the #key — in plaintext, the app's network_security_config.xml permits cleartext only to RFC-1918 (LAN) and *.ts.net (Tailscale) hosts; sshx itself stays HTTPS-only. See ../host/README.md for the privacy trade-off and setup.


Limitations

  • Requires the Android SDK to build here. This repo machine has no Gradle and no Android SDK; build in Android Studio (or any machine with the SDK + JDK 17).
  • The overlay couples to sshx's current DOM/classnames (.term-container, the bg-red-500 close dot, the Create new terminal button, the .cursor-nwse-resize handle). An sshx front-end change can break the overlay or the reflow handle math.
  • Zoom is a CSS scale (crisp at 100%, soft when zoomed): xterm's fontSize lives in unreachable Svelte closures. Crisp magnification needs a real resize, which is what the trusted-drag reflow does for fill, not arbitrary zoom.
  • The resolver fetch is cleartext over your overlay net only; do not point it at a public address (it would leak the E2E #key).
  • Pinned versions are all-or-nothing (Compose-compiler ↔ Kotlin coupling is exact). See SPEC.md §0.

Related