fix(android): auto-wake opt-out + console glyphs default by form factor

An mDNS discovery miss was forcing connects through the Wake-on-LAN wait
even for a host that's already up; add a Settings toggle ("Auto-wake on
connect") that skips the mDNS-liveness gate and dials straight through
when off.

Also default the console UI's button glyphs by form factor instead of
always starting in TV-remote style: a phone/tablet only ever enters the
console UI via a real controller, so it should show gamepad glyphs from
the first frame, not a remote's select/back glyphs. TV keeps the remote
default.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-06 19:56:31 +02:00
parent f94978f820
commit 0429e93167
4 changed files with 33 additions and 7 deletions
@@ -212,11 +212,12 @@ fun ConnectScreen(
} }
} }
// Wake-aware connect. If the target is a saved host with a learned MAC that ISN'T currently // Wake-aware connect. If auto-wake is on (Settings.autoWakeEnabled) and the target is a saved
// advertising (asleep/off), wake it and WAIT for it to reappear on mDNS (WakeController shows the // host with a learned MAC that ISN'T currently advertising (asleep/off, or just missing from
// "Waking…" overlay) before dialing — discovery stays running meanwhile so we can see it come // mDNS), wake it and WAIT for it to reappear on mDNS (WakeController shows the "Waking…" overlay)
// back. A fire-and-forget packet + the connect timeout wasn't enough for a cold boot. Otherwise // before dialing — discovery stays running meanwhile so we can see it come back. A fire-and-forget
// dial straight through. // packet + the connect timeout wasn't enough for a cold boot. Otherwise (auto-wake off, no MAC, or
// already seen live) dial straight through.
fun doConnect(targetHost: String, targetPort: Int, name: String, pinHex: String?) { fun doConnect(targetHost: String, targetPort: Int, name: String, pinHex: String?) {
if (identity == null) { if (identity == null) {
status = "Identity not ready yet — try again in a moment" status = "Identity not ready yet — try again in a moment"
@@ -230,7 +231,7 @@ fun ConnectScreen(
fun liveAdvert(): DiscoveredHost? = fun liveAdvert(): DiscoveredHost? =
if (kh != null) discovered.firstOrNull { kh.matches(it) } if (kh != null) discovered.firstOrNull { kh.matches(it) }
else discovered.firstOrNull { it.host == targetHost && it.port == targetPort } else discovered.firstOrNull { it.host == targetHost && it.port == targetPort }
if (macs.isNotEmpty() && liveAdvert() == null) { if (settings.autoWakeEnabled && macs.isNotEmpty() && liveAdvert() == null) {
waker.start( waker.start(
hostName = name, hostName = name,
connectsAfter = true, connectsAfter = true,
@@ -51,8 +51,12 @@ class MainActivity : ComponentActivity() {
* Whether the last console input came from a real gamepad (face buttons / stick) vs. a TV D-pad * Whether the last console input came from a real gamepad (face buttons / stick) vs. a TV D-pad
* remote (which has no A/B/X/Y). The console UI reads this to show glyphs the user recognises — pad * remote (which has no A/B/X/Y). The console UI reads this to show glyphs the user recognises — pad
* face buttons, or a select glyph + arrows for a remote. Compose observes it (a snapshot state). * face buttons, or a select glyph + arrows for a remote. Compose observes it (a snapshot state).
* Defaults to the remote glyphs on a TV (its D-pad remote is the typical first input, and often the
* only one) and to gamepad glyphs everywhere else (the console UI on a phone/tablet only activates
* via a real controller, so a TV-remote glyph would be a wrong first impression there) — set from
* [onCreate] once a [Context] is available, then kept live by real input.
*/ */
var lastPadIsGamepad by mutableStateOf(false) var lastPadIsGamepad by mutableStateOf(true)
private set private set
/** The panel's highest-refresh display mode (0 = unknown/unsupported), resolved once at startup. */ /** The panel's highest-refresh display mode (0 = unknown/unsupported), resolved once at startup. */
@@ -60,6 +64,7 @@ class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
lastPadIsGamepad = !isTvDevice(this)
resolveHighRefreshMode() resolveHighRefreshMode()
setConsoleHighRefreshRate(true) // the console UI wants max refresh; streaming manages its own setConsoleHighRefreshRate(true) // the console UI wants max refresh; streaming manages its own
// Dark, transparent system bars regardless of the system theme — our UI is always dark, so // Dark, transparent system bars regardless of the system theme — our UI is always dark, so
@@ -62,6 +62,15 @@ data class Settings(
* lock and HDMI game-mode signalling stay on regardless — they're harmless. * lock and HDMI game-mode signalling stay on regardless — they're harmless.
*/ */
val lowLatencyMode: Boolean = true, val lowLatencyMode: Boolean = true,
/**
* Wake-on-LAN a saved host before connecting when it isn't currently seen on mDNS. On (default):
* a connect to a host with a learned MAC that isn't advertising sends a magic packet and waits
* for it to reappear (see [WakeController]) before dialing. Off: always dial straight through,
* skipping the mDNS-presence check entirely — for a host that's actually up but not visible on
* mDNS (a flaky discovery path, a VLAN/subnet that blocks multicast, etc.), where auto-wake would
* otherwise misfire and wait out its timeout despite the host already being reachable.
*/
val autoWakeEnabled: Boolean = true,
) )
/** [Settings.touchMode] values; persisted by name. */ /** [Settings.touchMode] values; persisted by name. */
@@ -91,6 +100,7 @@ class SettingsStore(context: Context) {
gamepadUiEnabled = prefs.getBoolean(K_GAMEPAD_UI, true), gamepadUiEnabled = prefs.getBoolean(K_GAMEPAD_UI, true),
libraryEnabled = prefs.getBoolean(K_LIBRARY, true), libraryEnabled = prefs.getBoolean(K_LIBRARY, true),
lowLatencyMode = prefs.getBoolean(K_LOW_LATENCY, true), lowLatencyMode = prefs.getBoolean(K_LOW_LATENCY, true),
autoWakeEnabled = prefs.getBoolean(K_AUTO_WAKE, true),
) )
fun save(s: Settings) { fun save(s: Settings) {
@@ -110,6 +120,7 @@ class SettingsStore(context: Context) {
.putBoolean(K_GAMEPAD_UI, s.gamepadUiEnabled) .putBoolean(K_GAMEPAD_UI, s.gamepadUiEnabled)
.putBoolean(K_LIBRARY, s.libraryEnabled) .putBoolean(K_LIBRARY, s.libraryEnabled)
.putBoolean(K_LOW_LATENCY, s.lowLatencyMode) .putBoolean(K_LOW_LATENCY, s.lowLatencyMode)
.putBoolean(K_AUTO_WAKE, s.autoWakeEnabled)
.apply() .apply()
} }
@@ -129,6 +140,7 @@ class SettingsStore(context: Context) {
const val K_GAMEPAD_UI = "gamepad_ui_enabled" const val K_GAMEPAD_UI = "gamepad_ui_enabled"
const val K_LIBRARY = "library_enabled" const val K_LIBRARY = "library_enabled"
const val K_LOW_LATENCY = "low_latency_mode" const val K_LOW_LATENCY = "low_latency_mode"
const val K_AUTO_WAKE = "auto_wake_enabled"
/** Legacy Boolean the enum replaced — read once as the migration default, never written. */ /** Legacy Boolean the enum replaced — read once as the migration default, never written. */
const val K_TRACKPAD = "trackpad_mode" const val K_TRACKPAD = "trackpad_mode"
@@ -394,6 +394,14 @@ private fun InterfaceSettings(s: Settings, update: (Settings) -> Unit) {
checked = s.libraryEnabled, checked = s.libraryEnabled,
onCheckedChange = { on -> update(s.copy(libraryEnabled = on)) }, onCheckedChange = { on -> update(s.copy(libraryEnabled = on)) },
) )
ToggleRow(
title = "Auto-wake on connect",
subtitle = "Send Wake-on-LAN and wait for a saved host to reappear on mDNS before " +
"connecting. Turn off if a host that's already on isn't seen on mDNS, so connects " +
"go straight through instead of waiting out the wake timeout.",
checked = s.autoWakeEnabled,
onCheckedChange = { on -> update(s.copy(autoWakeEnabled = on)) },
)
ToggleRow( ToggleRow(
title = "Stats overlay", title = "Stats overlay",
subtitle = "Show FPS, throughput and latency while streaming (3-finger tap toggles it live)", subtitle = "Show FPS, throughput and latency while streaming (3-finger tap toggles it live)",