From 4a87cef98cca20c2b2baa9e5e4dbead281a641a4 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Sun, 5 Jul 2026 20:04:47 +0200 Subject: [PATCH] feat(android): console UI, wake-on-LAN wait-until-up, host edit + TV/tablet polish Bring the Android client to parity with Apple's gamepad experience and finish Wake-on-LAN. - Console/gamepad home: host carousel, aurora chrome, mTLS game-library coverflow, and an input-aware legend that switches between gamepad face buttons and a TV-remote select-ring + arrows based on the last-used input. - Wake-on-LAN: the fire-and-forget send is upgraded to wait-until-up (WakeController/WakeOverlay: resend + mDNS poll, 90s timeout, cancel/retry, fingerprint-matched so a host that cold-boots onto a new DHCP IP still connects), plus host edit (touch dialog + console form) with an auto-filled MAC. - Android TV: brand banner (android:banner), density-aware console scaling, D-pad/ remote nav (Up = Settings, Down or the pad Select button = host Options), emergency stream-exit chord, and 120Hz console refresh. - Touch UI: settings split into subpages with a tablet NavigationRail, axis-aware tab animation (horizontal on phones, vertical on the tablet rail), animated settings navigation, and a licenses screen with a back button + the real workspace version (read from Cargo.toml). - Vector Lock/controller icons (no emoji); bundled Geist font. Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitea/workflows/android.yml | 6 +- clients/android/README.md | 6 +- clients/android/app/build.gradle.kts | 37 +- .../android/app/src/main/AndroidManifest.xml | 1 + .../android/app/src/main/assets/GEIST-OFL.txt | 93 ++++ .../src/main/kotlin/io/unom/punktfunk/App.kt | 192 +++++-- .../io/unom/punktfunk/ConnectDialogs.kt | 76 ++- .../kotlin/io/unom/punktfunk/ConnectScreen.kt | 337 +++++++++--- .../io/unom/punktfunk/ControllersScreen.kt | 25 +- .../io/unom/punktfunk/GamepadAddHostScreen.kt | 467 +++++++++++++++++ .../kotlin/io/unom/punktfunk/GamepadChrome.kt | 345 +++++++++++++ .../io/unom/punktfunk/GamepadDialogs.kt | 357 +++++++++++++ .../kotlin/io/unom/punktfunk/GamepadHome.kt | 328 ++++++++++++ .../kotlin/io/unom/punktfunk/GamepadNav.kt | 257 +++++++++ .../unom/punktfunk/GamepadSettingsScreen.kt | 313 +++++++++++ .../kotlin/io/unom/punktfunk/GamepadUi.kt | 63 +++ .../kotlin/io/unom/punktfunk/LibraryScreen.kt | 297 +++++++++++ .../io/unom/punktfunk/LicensesScreen.kt | 83 ++- .../kotlin/io/unom/punktfunk/MainActivity.kt | 108 +++- .../main/kotlin/io/unom/punktfunk/Settings.kt | 19 + .../io/unom/punktfunk/SettingsScreen.kt | 486 +++++++++++++----- .../kotlin/io/unom/punktfunk/StreamScreen.kt | 4 + .../main/kotlin/io/unom/punktfunk/Theme.kt | 4 +- .../src/main/kotlin/io/unom/punktfunk/Type.kt | 44 ++ .../io/unom/punktfunk/WakeController.kt | 125 +++++ .../kotlin/io/unom/punktfunk/WakeOverlay.kt | 124 +++++ .../punktfunk/components/HostComponents.kt | 10 +- .../src/main/res/drawable-hdpi/tv_banner.png | Bin 0 -> 5109 bytes .../src/main/res/drawable-xhdpi/tv_banner.png | Bin 0 -> 6827 bytes .../main/res/drawable-xxhdpi/tv_banner.png | Bin 0 -> 9831 bytes .../app/src/main/res/font/geist_bold.otf | Bin 0 -> 166516 bytes .../app/src/main/res/font/geist_medium.otf | Bin 0 -> 162304 bytes .../app/src/main/res/font/geist_regular.otf | Bin 0 -> 157508 bytes .../app/src/main/res/font/geist_semibold.otf | Bin 0 -> 164780 bytes .../unom/punktfunk/screenshots/ShotScenes.kt | 2 +- clients/android/kit/build.gradle.kts | 17 +- .../io/unom/punktfunk/kit/GamepadFeedback.kt | 79 ++- .../io/unom/punktfunk/kit/library/Library.kt | 195 +++++++ .../punktfunk/kit/security/KnownHostStore.kt | 28 + .../kit/security/KnownHostStoreTest.kt | 33 ++ clients/android/native/Cargo.toml | 6 +- clients/android/native/src/decode.rs | 41 +- 42 files changed, 4247 insertions(+), 361 deletions(-) create mode 100644 clients/android/app/src/main/assets/GEIST-OFL.txt create mode 100644 clients/android/app/src/main/kotlin/io/unom/punktfunk/GamepadAddHostScreen.kt create mode 100644 clients/android/app/src/main/kotlin/io/unom/punktfunk/GamepadChrome.kt create mode 100644 clients/android/app/src/main/kotlin/io/unom/punktfunk/GamepadDialogs.kt create mode 100644 clients/android/app/src/main/kotlin/io/unom/punktfunk/GamepadHome.kt create mode 100644 clients/android/app/src/main/kotlin/io/unom/punktfunk/GamepadNav.kt create mode 100644 clients/android/app/src/main/kotlin/io/unom/punktfunk/GamepadSettingsScreen.kt create mode 100644 clients/android/app/src/main/kotlin/io/unom/punktfunk/GamepadUi.kt create mode 100644 clients/android/app/src/main/kotlin/io/unom/punktfunk/LibraryScreen.kt create mode 100644 clients/android/app/src/main/kotlin/io/unom/punktfunk/Type.kt create mode 100644 clients/android/app/src/main/kotlin/io/unom/punktfunk/WakeController.kt create mode 100644 clients/android/app/src/main/kotlin/io/unom/punktfunk/WakeOverlay.kt create mode 100644 clients/android/app/src/main/res/drawable-hdpi/tv_banner.png create mode 100644 clients/android/app/src/main/res/drawable-xhdpi/tv_banner.png create mode 100644 clients/android/app/src/main/res/drawable-xxhdpi/tv_banner.png create mode 100644 clients/android/app/src/main/res/font/geist_bold.otf create mode 100644 clients/android/app/src/main/res/font/geist_medium.otf create mode 100644 clients/android/app/src/main/res/font/geist_regular.otf create mode 100644 clients/android/app/src/main/res/font/geist_semibold.otf create mode 100644 clients/android/kit/src/main/kotlin/io/unom/punktfunk/kit/library/Library.kt create mode 100644 clients/android/kit/src/test/kotlin/io/unom/punktfunk/kit/security/KnownHostStoreTest.kt diff --git a/.gitea/workflows/android.yml b/.gitea/workflows/android.yml index d9b2248..e708a91 100644 --- a/.gitea/workflows/android.yml +++ b/.gitea/workflows/android.yml @@ -1,5 +1,5 @@ # Android client CI (Gitea Actions). Builds the Rust JNI core (clients/android/native) via -# cargo-ndk for both shipping ABIs and assembles the debug APK (clients/android). Mirrors apple.yml +# cargo-ndk for all three shipping ABIs and assembles the debug APK (clients/android). Mirrors apple.yml # but on a Linux runner — the NDK is cross-platform, so no self-hosted host is needed. # # Prereq: the runner needs ~6 GB free + internet (it pulls the Android SDK/NDK and the Gradle @@ -40,7 +40,7 @@ jobs: fi RUSTUP="$(command -v rustup || echo "$HOME/.cargo/bin/rustup")" dirname "$RUSTUP" >> "$GITHUB_PATH" - "$RUSTUP" target add aarch64-linux-android x86_64-linux-android + "$RUSTUP" target add aarch64-linux-android armv7-linux-androideabi x86_64-linux-android - name: Android SDK uses: android-actions/setup-android@v3 @@ -98,7 +98,7 @@ jobs: RELEASE_KEY_PASSWORD: ${{ secrets.RELEASE_KEY_PASSWORD }} run: | echo "${{ secrets.RELEASE_KEYSTORE_BASE64 }}" | base64 -d > release.jks - # AAB for Play; a universal APK (both ABIs) for direct sideload/testing — same upload key. + # AAB for Play; a universal APK (all ABIs) for direct sideload/testing — same upload key. ./gradlew :app:bundleRelease :app:assembleRelease --stacktrace # Publish BEFORE the Play upload so artifacts land even while the Play step is still failing. diff --git a/clients/android/README.md b/clients/android/README.md index ce8b4d2..8ba01ce 100644 --- a/clients/android/README.md +++ b/clients/android/README.md @@ -16,7 +16,9 @@ couch (D-pad / gamepad focus navigation). pairing** (or TOFU on trusted LANs), then reconnects on a Keystore-wrapped, pinned identity. - **Compose UI** — Connect / Settings / Stream screens with Material You theming. -Built for `arm64-v8a` + `x86_64`. +Built for `arm64-v8a` + `armeabi-v7a` + `x86_64` — the 32-bit `armeabi-v7a` slice is what keeps the +app installable on the many 32-bit Google TV / Android TV streamers (Walmart onn. 4K, Chromecast with +Google TV, budget Amlogic boxes) that otherwise reject a 64-bit-only build as "not compatible". ## Get it @@ -54,7 +56,7 @@ kit/ :kit — NativeBridge · native mDNS discovery · Gamepad · K **Prerequisites:** Android SDK + **NDK r30** (`30.0.14904198`), `platforms;android-37.0`, `build-tools;37.0.0`, **`cmake;3.22.1`** (builds libopus); **JDK 21** (AGP 9.2 runs on JDK 17–21, not -a newer default); Rust with `rustup target add aarch64-linux-android x86_64-linux-android` and +a newer default); Rust with `rustup target add aarch64-linux-android armv7-linux-androideabi x86_64-linux-android` and `cargo install cargo-ndk`. Toolchain is pinned (AGP 9.2 · Gradle 9.4.1 · Kotlin 2.3.21 · Compose BOM 2026.05.01 · compileSdk 37 · minSdk 31). diff --git a/clients/android/app/build.gradle.kts b/clients/android/app/build.gradle.kts index a732f57..9c388a7 100644 --- a/clients/android/app/build.gradle.kts +++ b/clients/android/app/build.gradle.kts @@ -22,14 +22,34 @@ android { } applicationId = "io.unom.punktfunk" - minSdk = 31 + // Android 9. Reaches older Android TV boxes (e.g. Amlogic streamers still on Android 9–11); + // the handful of API 31+ APIs we use are runtime-gated (Material You → brand palette, rumble + // → legacy Vibrator, NEARBY_WIFI/lights/ADPF already gated), so nothing is lost above 28. + minSdk = 28 targetSdk = 36 val vCode = (props.getProperty("VERSION_CODE") ?: System.getenv("VERSION_CODE")) versionCode = vCode?.toInt() ?: 1 // versionName is the single project version, threaded from CI (a vX.Y.Z release or a // canary string). versionCode stays the monotonic run number (Play rejects regressions). - versionName = (props.getProperty("VERSION_NAME") ?: System.getenv("VERSION_NAME")) ?: "0.0.2" - ndk { abiFilters += listOf("arm64-v8a", "x86_64") } + // Local dev (no VERSION_NAME) falls back to the workspace version from the root Cargo.toml — + // the single source of truth — so an on-device build shows the real current version, not a + // stale placeholder. + val workspaceVersion = runCatching { + project.rootProject.file("../../Cargo.toml").readLines() + .dropWhile { !it.trim().startsWith("[workspace.package]") } + .firstOrNull { it.trim().startsWith("version") } + ?.substringAfter('=')?.trim()?.trim('"') + }.getOrNull() + versionName = (props.getProperty("VERSION_NAME") ?: System.getenv("VERSION_NAME")) + ?: workspaceVersion ?: "0.0.0" + // Ship 32-bit armeabi-v7a alongside 64-bit arm64-v8a: many Google TV / Android TV streamers + // (Walmart onn. 4K, Chromecast with Google TV, budget Amlogic boxes) run a 32-bit Android + // userspace, and because this app carries native code, Google Play (and a sideload installer) + // filters it as "not compatible" on those devices unless an armeabi-v7a variant is present. + // x86_64 stays for the emulator. Google keeps delivering to 32-bit TV devices (see the Aug + // 2025 "64-bit app compatibility for Google TV and Android TV" post) — the 64-bit lib is the + // required half; the 32-bit lib is what actually reaches the boxes people report failing. + ndk { abiFilters += listOf("arm64-v8a", "armeabi-v7a", "x86_64") } } signingConfigs { @@ -97,9 +117,18 @@ dependencies { implementation("androidx.compose.ui:ui-tooling-preview") implementation("androidx.compose.foundation:foundation") implementation("androidx.compose.material3:material3") - implementation("androidx.compose.material:material-icons-core") // bottom-bar tab icons + implementation("androidx.compose.material:material-icons-core") // bottom-bar / rail tab icons + implementation("androidx.compose.material:material-icons-extended") // settings-category icons debugImplementation("androidx.compose.ui:ui-tooling") + // Cover-art loading for the game-library coverflow. Coil 2.x uses OkHttp under the hood, so we + // feed it the same mTLS OkHttpClient the library fetch uses (reaching the host's own art proxy). + implementation("io.coil-kt:coil-compose:2.7.0") + + // Real backdrop blur for the floating console legends (RenderEffect on API 31+, a translucent + // scrim below). The gamepad UI's frosted pills sample + blur whatever scrolls behind them. + implementation("dev.chrisbanes.haze:haze:1.6.0") + // Android TV components (we target phone + TV) land in the TV-UI milestone: // implementation("androidx.tv:tv-material:1.1.0") // The manifest already declares leanback so the scaffold installs on TV. diff --git a/clients/android/app/src/main/AndroidManifest.xml b/clients/android/app/src/main/AndroidManifest.xml index 6edf87d..d51e90b 100644 --- a/clients/android/app/src/main/AndroidManifest.xml +++ b/clients/android/app/src/main/AndroidManifest.xml @@ -36,6 +36,7 @@ - NavigationBarItem( - selected = tab == t, - onClick = { tab = t }, - icon = { Icon(t.icon, contentDescription = t.label) }, - label = { Text(t.label) }, - ) - } - } - }, - ) { innerPadding -> - Box(Modifier.fillMaxSize().padding(innerPadding)) { - AnimatedContent( - targetState = tab, - transitionSpec = { - if (targetState.ordinal > initialState.ordinal) { + // Adaptive nav: a bottom bar on phones; on tablets / large windows a side NavigationRail + // with its items centred vertically (the common Android tablet idiom, mirroring iPad's + // side navigation). A short landscape phone keeps the bottom bar (rail needs height too). + // Tabs slide along the axis the nav sits on: horizontally with the bottom bar (phone), + // vertically with the side rail (tablet), so the motion tracks the direction you moved. + val tabContent: @Composable (vertical: Boolean) -> Unit = { vertical -> + AnimatedContent( + targetState = tab, + transitionSpec = { + val forward = targetState.ordinal > initialState.ordinal + when { + vertical && forward -> + slideInVertically { it } + fadeIn() togetherWith + slideOutVertically { -it } + fadeOut() + vertical -> + slideInVertically { -it } + fadeIn() togetherWith + slideOutVertically { it } + fadeOut() + forward -> slideInHorizontally { it } + fadeIn() togetherWith slideOutHorizontally { -it } + fadeOut() - } else { + else -> slideInHorizontally { -it } + fadeIn() togetherWith slideOutHorizontally { it } + fadeOut() + } + }, + label = "TabTransition" + ) { targetTab -> + when (targetTab) { + Tab.Connect -> ConnectScreen(settings = settings, onConnected = { streamHandle = it }) + Tab.Settings -> SettingsScreen( + initial = settings, + onChange = { settings = it; settingsStore.save(it) }, + onBack = { tab = Tab.Connect }, + ) + } + } + } + + BoxWithConstraints(Modifier.fillMaxSize()) { + if (maxWidth >= 600.dp && maxHeight >= 480.dp) { + Row(Modifier.fillMaxSize()) { + NavigationRail(Modifier.fillMaxHeight()) { + Spacer(Modifier.weight(1f)) // centre the rail items vertically + Tab.entries.forEach { t -> + NavigationRailItem( + selected = tab == t, + onClick = { tab = t }, + icon = { Icon(t.icon, contentDescription = t.label) }, + label = { Text(t.label) }, + ) + } + Spacer(Modifier.weight(1f)) + } + // The rail handles its own insets; the content pane insets itself (the screens + // don't, since they used to rely on the Scaffold's padding). + Box(Modifier.weight(1f).fillMaxHeight().systemBarsPadding()) { tabContent(true) } + } + } else { + Scaffold( + bottomBar = { + NavigationBar { + Tab.entries.forEach { t -> + NavigationBarItem( + selected = tab == t, + onClick = { tab = t }, + icon = { Icon(t.icon, contentDescription = t.label) }, + label = { Text(t.label) }, + ) + } } }, - label = "TabTransition" - ) { targetTab -> - when (targetTab) { - Tab.Connect -> ConnectScreen(settings = settings, onConnected = { streamHandle = it }) - Tab.Settings -> SettingsScreen( - initial = settings, - onChange = { settings = it; settingsStore.save(it) }, - onBack = { tab = Tab.Connect }, - ) - } + ) { innerPadding -> + Box(Modifier.fillMaxSize().padding(innerPadding)) { tabContent(false) } } } } } } } + +/** Which console screen the gamepad shell is showing. */ +private enum class GamepadScreen { Home, Settings, Library } + +/** + * The console (gamepad) shell — the Android mirror of the Apple client's ContentView gamepad branch: + * a full-screen host carousel with X → Settings and Y → a saved host's library, all sharing + * [ConnectScreen]'s connect logic. No bottom bar; navigation is button-driven. + */ +@Composable +fun GamepadShell( + settings: Settings, + onSettingsChange: (Settings) -> Unit, + onConnected: (Long) -> Unit, +) { + val context = LocalContext.current + var screen by remember { mutableStateOf(GamepadScreen.Home) } + var libraryHost by remember { mutableStateOf(null) } + + // On a TV, shrink the 10-foot UI so its elements aren't oversized. Density-aware: expand the + // effective dp footprint to at least CONSOLE_TV_MIN_WIDTH_DP (→ smaller elements) ONLY when the + // panel reports fewer dp than that; a low-density TV that's already spacious, and every phone / + // tablet, keep their real density unchanged. This is the "based on pixel density" scale the layout + // wanted — one uniform factor across text, cards, spacing, and insets. + val isTv = remember { isTvDevice(context) } + val baseDensity = LocalDensity.current + val screenWidthPx = LocalConfiguration.current.screenWidthDp * baseDensity.density + val fitDensity = screenWidthPx / CONSOLE_TV_MIN_WIDTH_DP + val consoleDensity = if (isTv && fitDensity < baseDensity.density) fitDensity else baseDensity.density + + CompositionLocalProvider(LocalDensity provides Density(consoleDensity, baseDensity.fontScale)) { + // Cross-fade between console screens so switches are smooth. Each slot's controller nav is gated + // on being the CURRENT target (`s == screen`), so during the fade only the incoming screen drives + // the pad. All screens pin their legend at the same ConsoleLegendInset, so it reads as fixed while + // the content behind it fades. + Crossfade(targetState = screen, animationSpec = tween(240), label = "consoleScreen") { s -> + when (s) { + GamepadScreen.Home -> ConnectScreen( + settings = settings, + onConnected = onConnected, + gamepadUi = true, + onOpenSettings = { screen = GamepadScreen.Settings }, + onOpenLibrary = { host -> libraryHost = host; screen = GamepadScreen.Library }, + navGate = s == screen, + ) + GamepadScreen.Settings -> GamepadSettingsScreen( + initial = settings, + onChange = onSettingsChange, + onBack = { screen = GamepadScreen.Home }, + navActive = s == screen, + ) + GamepadScreen.Library -> libraryHost?.let { host -> + LibraryScreen( + host = host, + onBack = { screen = GamepadScreen.Home; libraryHost = null }, + navActive = s == screen, + ) + } ?: run { screen = GamepadScreen.Home } + } + } + } +} + +/** Minimum effective dp width the console UI targets on a TV (bigger → the 10-foot UI shrinks). */ +private const val CONSOLE_TV_MIN_WIDTH_DP = 1180f diff --git a/clients/android/app/src/main/kotlin/io/unom/punktfunk/ConnectDialogs.kt b/clients/android/app/src/main/kotlin/io/unom/punktfunk/ConnectDialogs.kt index f6f4be1..8a3bc3f 100644 --- a/clients/android/app/src/main/kotlin/io/unom/punktfunk/ConnectDialogs.kt +++ b/clients/android/app/src/main/kotlin/io/unom/punktfunk/ConnectDialogs.kt @@ -9,7 +9,9 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.CircularProgressIndicator @@ -33,6 +35,7 @@ import androidx.compose.ui.unit.dp import io.unom.punktfunk.kit.NativeBridge import io.unom.punktfunk.kit.security.ClientIdentity import io.unom.punktfunk.kit.security.KnownHost +import io.unom.punktfunk.kit.security.KnownHostStore import io.unom.punktfunk.models.PendingTrust import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -320,32 +323,75 @@ internal fun AwaitingApprovalDialog(hostLabel: String, onCancel: () -> Unit) { } /** - * Rename a saved host's label (discovered hosts are named by mDNS; this is how you give one a - * friendly name like "Living Room" after pairing). Keyed by the host so reopening resets the field. + * Edit a saved host: name, address, port, and the Wake-on-LAN MAC. The MAC is auto-learned from the + * host's mDNS advert while it's online, but this is where you can enter or correct it (e.g. to wake a + * host you've only ever reached by address). [suggestedMacs] prefills the field from the live advert + * when nothing's been learned yet. Keyed by the host so reopening resets the fields. Mirrors the + * Apple client's edit form. */ @Composable -internal fun RenameHostDialog( +internal fun EditHostDialog( target: KnownHost, - onRename: (String) -> Unit, + suggestedMacs: List, + onSave: (KnownHost) -> Unit, onDismiss: () -> Unit, ) { - var newName by remember(target) { mutableStateOf(target.name) } + var name by remember(target) { mutableStateOf(target.name) } + var address by remember(target) { mutableStateOf(target.address) } + var port by remember(target) { mutableStateOf(target.port.toString()) } + var mac by remember(target) { + mutableStateOf(target.mac.ifEmpty { suggestedMacs }.joinToString(", ")) + } AlertDialog( onDismissRequest = onDismiss, - title = { Text("Rename host") }, + title = { Text("Edit host") }, text = { - OutlinedTextField( - value = newName, - onValueChange = { newName = it }, - label = { Text("Name") }, - placeholder = { Text(target.address) }, - singleLine = true, - ) + Column( + modifier = Modifier.verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + OutlinedTextField( + value = name, + onValueChange = { name = it }, + label = { Text("Name") }, + placeholder = { Text(target.address) }, + singleLine = true, + ) + OutlinedTextField( + value = address, + onValueChange = { address = it }, + label = { Text("Address") }, + singleLine = true, + ) + OutlinedTextField( + value = port, + onValueChange = { v -> port = v.filter { it.isDigit() }.take(5) }, + label = { Text("Port") }, + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + ) + OutlinedTextField( + value = mac, + onValueChange = { mac = it }, + label = { Text("Wake-on-LAN MAC") }, + placeholder = { Text("auto-filled when the host is seen") }, + singleLine = true, + ) + } }, confirmButton = { TextButton( - enabled = newName.isNotBlank(), - onClick = { onRename(newName.trim()) }, + enabled = address.isNotBlank(), + onClick = { + onSave( + target.copy( + name = name.trim().ifEmpty { target.address }, + address = address.trim(), + port = port.toIntOrNull() ?: target.port, + mac = KnownHostStore.parseMacs(mac), + ), + ) + }, ) { Text("Save") } }, dismissButton = { diff --git a/clients/android/app/src/main/kotlin/io/unom/punktfunk/ConnectScreen.kt b/clients/android/app/src/main/kotlin/io/unom/punktfunk/ConnectScreen.kt index 8b81305..2be7467 100644 --- a/clients/android/app/src/main/kotlin/io/unom/punktfunk/ConnectScreen.kt +++ b/clients/android/app/src/main/kotlin/io/unom/punktfunk/ConnectScreen.kt @@ -84,7 +84,17 @@ private class RequestAccessState(val target: PendingTrust) { } @Composable -fun ConnectScreen(settings: Settings, onConnected: (Long) -> Unit) { +fun ConnectScreen( + settings: Settings, + onConnected: (Long) -> Unit, + // Console (gamepad) mode: render the host carousel instead of the touch grid, sharing all of this + // screen's connect/trust/discovery logic. [onOpenSettings]/[onOpenLibrary] are the X/Y actions the + // gamepad shell owns (the touch UI reaches Settings via the bottom bar and has no library button). + gamepadUi: Boolean = false, + onOpenSettings: () -> Unit = {}, + onOpenLibrary: (KnownHost) -> Unit = {}, + navGate: Boolean = true, // false while the console home is cross-fading out +) { val scope = rememberCoroutineScope() val context = LocalContext.current var host by remember { mutableStateOf("") } @@ -124,6 +134,10 @@ fun ConnectScreen(settings: Settings, onConnected: (Long) -> Unit) { val identityStore = remember { IdentityStore(context) } val knownHostStore = remember { KnownHostStore(context) } var savedHosts by remember { mutableStateOf(knownHostStore.all()) } + // Wakes a sleeping saved host and waits for it to reappear on mDNS before dialing (its overlay + // rides over both the touch and console home). Fire-and-forget WoL isn't enough — a cold boot can + // take a minute-plus to advertise again. + val waker = remember { WakeController(scope) } // Learn wake MAC(s) from live adverts for hosts we've saved (parity with the desktop clients), // so we can Wake-on-LAN them once they sleep. Runs only when the discovered set changes; the // prefs write is guarded (no-op when unchanged), and we refresh the saved list only if a MAC @@ -156,8 +170,11 @@ fun ConnectScreen(settings: Settings, onConnected: (Long) -> Unit) { var pendingTrust by remember { mutableStateOf(null) } // A no-PIN "request access" connect in flight (the cancelable "Waiting for approval…" dialog). var awaiting by remember { mutableStateOf(null) } - // A saved host whose label is being edited (the Rename dialog). - var renameTarget by remember { mutableStateOf(null) } + // A saved host being edited (name / address / port / MAC). + var editTarget by remember { mutableStateOf(null) } + // A saved host whose console options menu (Wake / Edit / Forget) is open — reached with Up on the + // carousel (the console counterpart of the touch host card's overflow menu). + var optionsTarget by remember { mutableStateOf(null) } // Discovered hosts not already saved — a saved host (paired or TOFU) belongs in "Saved hosts", // not also in "Discovered", so we hide the overlap (matched by fingerprint when both carry it, so @@ -184,25 +201,16 @@ fun ConnectScreen(settings: Settings, onConnected: (Long) -> Unit) { } } - // Issue the actual connect with identity + (optional) pin. On a TOFU connect (pinHex null), - // pin the fingerprint the host presented (as an unpaired known host) so the next connect goes - // straight through and it appears in the saved-hosts list. - fun doConnect(targetHost: String, targetPort: Int, name: String, pinHex: String?) { - val id = identity - if (id == null) { + // The actual dial (identity already ready). On a TOFU connect (pinHex null), pin the fingerprint + // the host presented (as an unpaired known host) so the next connect goes straight through and it + // appears in the saved-hosts list. + fun doConnectDirect(targetHost: String, targetPort: Int, name: String, pinHex: String?) { + val id = identity ?: run { status = "Identity not ready yet — try again in a moment" return } connecting = true status = "Connecting to $targetHost:$targetPort…" - // Auto-wake: reconnecting to a saved host that may be asleep. If we learned its MAC while it - // was online and it isn't currently advertising, fire a magic packet first — the connect's - // own timeout gives a woken host time to come up (harmless if it's already awake). - knownHostStore.get(targetHost, targetPort)?.mac - ?.takeIf { it.isNotEmpty() && discovered.none { d -> d.host == targetHost && d.port == targetPort } } - ?.let { macs -> - scope.launch(Dispatchers.IO) { NativeBridge.nativeWakeOnLan(macs.joinToString(","), targetHost) } - } discovery.stop() // free the Wi-Fi radio before the stream session scope.launch { val handle = connectNative(id, targetHost, targetPort, pinHex ?: "", CONNECT_TIMEOUT_MS) @@ -222,6 +230,47 @@ fun ConnectScreen(settings: Settings, onConnected: (Long) -> Unit) { } } + // Wake-aware connect. If the target is a saved host with a learned MAC that ISN'T currently + // advertising (asleep/off), wake it and WAIT for it to reappear on mDNS (WakeController shows the + // "Waking…" overlay) before dialing — discovery stays running meanwhile so we can see it come + // back. A fire-and-forget packet + the connect timeout wasn't enough for a cold boot. Otherwise + // dial straight through. + fun doConnect(targetHost: String, targetPort: Int, name: String, pinHex: String?) { + if (identity == null) { + status = "Identity not ready yet — try again in a moment" + return + } + val kh = knownHostStore.get(targetHost, targetPort) + val macs = kh?.mac ?: emptyList() + // "Up" = a live advert that is THIS host — matched by fingerprint first (so it survives a DHCP + // address change on a cold boot), else by address:port. Returns the CURRENT advert so we can + // dial its live address rather than the stale saved one. + fun liveAdvert(): DiscoveredHost? = + if (kh != null) discovered.firstOrNull { kh.matches(it) } + else discovered.firstOrNull { it.host == targetHost && it.port == targetPort } + if (macs.isNotEmpty() && liveAdvert() == null) { + waker.start( + hostName = name, + connectsAfter = true, + macs = macs, + lastIp = targetHost, + isOnline = { liveAdvert() != null }, + onOnline = { + val live = liveAdvert() + // Woke back on a new address? Re-key the saved record so it (and future connects) + // point at the live one, then dial there. + if (live != null && kh != null && (live.host != kh.address || live.port != kh.port)) { + knownHostStore.update(kh.address, kh.port, kh.copy(address = live.host, port = live.port)) + savedHosts = knownHostStore.all() + } + doConnectDirect(live?.host ?: targetHost, live?.port ?: targetPort, name, pinHex) + }, + ) + } else { + doConnectDirect(targetHost, targetPort, name, pinHex) + } + } + // The no-PIN "request access" path (delegated approval): open a normal identified connect that // the host PARKS until the operator clicks Approve in its console/web UI, showing a cancelable // "Waiting for approval…" dialog meanwhile. The SAME connection is admitted on approval (no @@ -304,7 +353,62 @@ fun ConnectScreen(settings: Settings, onConnected: (Long) -> Unit) { var showManualSheet by remember { mutableStateOf(false) } - Box(Modifier.fillMaxSize()) { + if (gamepadUi) { + // Console mode: the host carousel (saved → discovered → Add Host), driven by the pad. Shares + // every action above; the trailing Add Host tile opens the same manual-entry sheet. + val tiles = buildList { + savedHosts.forEach { kh -> + add( + HomeTile( + id = "saved-${kh.address}:${kh.port}", + title = kh.name, + subtitle = "${kh.address}:${kh.port}", + filled = true, + online = discovered.any { it.host == kh.address && it.port == kh.port }, + paired = kh.paired, + knownHost = kh, + activate = { connect(kh.address, kh.port) }, + ), + ) + } + discoveredUnsaved.forEach { dh -> + add( + HomeTile( + id = "disc-${dh.host}:${dh.port}", + title = dh.name, + subtitle = "${dh.host}:${dh.port}", + online = true, + activate = { connect(dh.host, dh.port, dh) }, + ), + ) + } + add( + HomeTile( + id = "add", + title = "Add Host", + subtitle = "Register a host by address", + isAdd = true, + activate = { showManualSheet = true }, + ), + ) + } + GamepadHome( + tiles = tiles, + libraryEnabled = settings.libraryEnabled, + controllerName = io.unom.punktfunk.kit.Gamepad.firstPad()?.name, + // Stop the carousel from consuming the pad while a sheet/dialog/overlay owns the screen, + // while a connect is in flight (else a second A launches a concurrent connect that leaks a + // handle — the touch grid guards the same way with enabled=!connecting), or while the whole + // console home is cross-fading out. + navActive = navGate && !connecting && !showManualSheet && pendingTrust == null && + awaiting == null && editTarget == null && optionsTarget == null && waker.waking == null, + onActivate = { it.activate() }, + onOpenLibrary = { it.knownHost?.let(onOpenLibrary) }, + onOpenSettings = onOpenSettings, + onOptions = { it.knownHost?.let { kh -> optionsTarget = kh } }, + ) + } else { + Box(Modifier.fillMaxSize()) { LazyVerticalGrid( columns = GridCells.Adaptive(minSize = 160.dp), modifier = Modifier.fillMaxSize(), @@ -385,13 +489,22 @@ fun ConnectScreen(settings: Settings, onConnected: (Long) -> Unit) { knownHostStore.remove(kh.address, kh.port) savedHosts = knownHostStore.all() }, - onRename = { renameTarget = kh }, - // Explicit wake: offered only when the host is offline and we have a MAC to - // target (a tap-to-connect already auto-wakes an offline saved host). - onWake = if (kh.mac.isNotEmpty() && - discovered.none { it.host == kh.address && it.port == kh.port } - ) { - { scope.launch(Dispatchers.IO) { NativeBridge.nativeWakeOnLan(kh.mac.joinToString(","), kh.address) } } + onEdit = { editTarget = kh }, + // Explicit wake-only: offered when the host is offline and we have a MAC. Runs + // through the WakeController so it shows the "Waking…" overlay and waits for + // the host to come online (matched by fingerprint, so a new DHCP address on a + // cold boot still counts as "up") rather than firing a single silent packet. + onWake = if (kh.mac.isNotEmpty() && discovered.none { kh.matches(it) }) { + { + waker.start( + hostName = kh.name, + connectsAfter = false, + macs = kh.mac, + lastIp = kh.address, + isOnline = { discovered.any { kh.matches(it) } }, + onOnline = {}, + ) + } } else { null }, @@ -451,80 +564,134 @@ fun ConnectScreen(settings: Settings, onConnected: (Long) -> Unit) { .align(Alignment.BottomEnd) .padding(20.dp), ) + } } if (showManualSheet) { - AddHostSheet( - hostName = hostName, - onHostNameChange = { hostName = it }, - host = host, - onHostChange = { host = it }, - port = port, - onPortChange = { port = it }, - connecting = connecting, - modeLabel = "$w×$h@$hz", - onDismiss = { showManualSheet = false }, - onConnect = { h2, p, n -> connect(h2, p, manualName = n) }, - ) - } - - pendingTrust?.let { pt -> - when (pt.kind) { - PendingTrust.Kind.TRUST_NEW -> TrustNewHostDialog( - pt = pt, - onTrust = { pendingTrust = null; doConnect(pt.host, pt.port, pt.name, null) }, - onPairInstead = { pendingTrust = pt.copy(kind = PendingTrust.Kind.PAIR) }, - onDismiss = { pendingTrust = null }, - ) - PendingTrust.Kind.FP_CHANGED -> FingerprintChangedDialog( - pt = pt, - onRepair = { pendingTrust = pt.copy(kind = PendingTrust.Kind.PAIR) }, - onDismiss = { pendingTrust = null }, - ) - PendingTrust.Kind.REQUEST_ACCESS -> RequestAccessDialog( - pt = pt, - onRequestAccess = { pendingTrust = null; requestAccess(pt) }, - onUsePin = { pendingTrust = pt.copy(kind = PendingTrust.Kind.PAIR) }, - onDismiss = { pendingTrust = null }, - ) - PendingTrust.Kind.PAIR -> PairPinDialog( - pt = pt, - identity = identity, - onPaired = { fp -> - // Verified host fp — save as a paired known host, then connect pinned. - knownHostStore.save(KnownHost(pt.host, pt.port, pt.name, fp, paired = true)) - savedHosts = knownHostStore.all() - pendingTrust = null - doConnect(pt.host, pt.port, pt.name, fp) + if (gamepadUi) { + // Console add-host: field list + on-screen controller keyboard. "Add" connects (which + // saves the host on TOFU/pair), exactly like the touch sheet's Connect. + GamepadAddHostScreen( + onAdd = { n, addr, p -> + showManualSheet = false + connect(addr, p, manualName = n) }, - onDismiss = { pendingTrust = null }, + onDismiss = { showManualSheet = false }, + ) + } else { + AddHostSheet( + hostName = hostName, + onHostNameChange = { hostName = it }, + host = host, + onHostChange = { host = it }, + port = port, + onPortChange = { port = it }, + connecting = connecting, + modeLabel = "$w×$h@$hz", + onDismiss = { showManualSheet = false }, + onConnect = { h2, p, n -> connect(h2, p, manualName = n) }, ) } } + pendingTrust?.let { pt -> + // Same trust/pairing logic, console-styled + controller-navigable in gamepad mode. + val onPair = { pendingTrust = pt.copy(kind = PendingTrust.Kind.PAIR) } + val onSavePaired = { fp: String -> + knownHostStore.save(KnownHost(pt.host, pt.port, pt.name, fp, paired = true)) + savedHosts = knownHostStore.all() + pendingTrust = null + doConnect(pt.host, pt.port, pt.name, fp) + } + when (pt.kind) { + PendingTrust.Kind.TRUST_NEW -> + if (gamepadUi) GamepadTrustNewDialog(pt, { pendingTrust = null; doConnect(pt.host, pt.port, pt.name, null) }, onPair, { pendingTrust = null }) + else TrustNewHostDialog(pt, { pendingTrust = null; doConnect(pt.host, pt.port, pt.name, null) }, onPair, { pendingTrust = null }) + PendingTrust.Kind.FP_CHANGED -> + if (gamepadUi) GamepadFingerprintChangedDialog(pt, onPair, { pendingTrust = null }) + else FingerprintChangedDialog(pt, onPair, { pendingTrust = null }) + PendingTrust.Kind.REQUEST_ACCESS -> + if (gamepadUi) GamepadRequestAccessDialog(pt, { pendingTrust = null; requestAccess(pt) }, onPair, { pendingTrust = null }) + else RequestAccessDialog(pt, { pendingTrust = null; requestAccess(pt) }, onPair, { pendingTrust = null }) + PendingTrust.Kind.PAIR -> + if (gamepadUi) GamepadPairPinDialog(pt, identity, onSavePaired, { pendingTrust = null }) + else PairPinDialog(pt, identity, onSavePaired, { pendingTrust = null }) + } + } + awaiting?.let { req -> - AwaitingApprovalDialog( - hostLabel = req.target.name, - onCancel = { - req.cancelled.set(true) - awaiting = null - connecting = false - discovery.start() // the request may still be pending on the host; keep scanning + val onCancel = { + req.cancelled.set(true) + awaiting = null + connecting = false + discovery.start() // the request may still be pending on the host; keep scanning + } + if (gamepadUi) GamepadAwaitingApprovalDialog(req.target.name, onCancel) + else AwaitingApprovalDialog(hostLabel = req.target.name, onCancel = onCancel) + } + + // Console host options (Up on a saved carousel tile): Wake / Edit / Forget. + optionsTarget?.let { kh -> + val offline = discovered.none { kh.matches(it) } + GamepadHostOptionsDialog( + hostName = kh.name, + canWake = kh.mac.isNotEmpty() && offline, + onWake = { + optionsTarget = null + waker.start( + hostName = kh.name, connectsAfter = false, macs = kh.mac, lastIp = kh.address, + isOnline = { discovered.any { kh.matches(it) } }, + onOnline = {}, + ) }, + // A saved host always has a library (it's a knownHost) → offer it when the setting's on, + // so a TV remote reaches the library here instead of via the Y face button. + onLibrary = if (settings.libraryEnabled) { + { optionsTarget = null; onOpenLibrary(kh) } + } else { + null + }, + onEdit = { optionsTarget = null; editTarget = kh }, + onForget = { + knownHostStore.remove(kh.address, kh.port) + savedHosts = knownHostStore.all() + optionsTarget = null + }, + onDismiss = { optionsTarget = null }, ) } - renameTarget?.let { kh -> - RenameHostDialog( - target = kh, - onRename = { newName -> - knownHostStore.rename(kh.address, kh.port, newName) - savedHosts = knownHostStore.all() - renameTarget = null - }, - onDismiss = { renameTarget = null }, - ) + editTarget?.let { kh -> + // Prefill a not-yet-learned MAC from the host's live advert, mirroring Apple's + // `discovery.hosts.first { host.matches($0) }?.macAddresses`. + val suggested = discovered.firstOrNull { kh.matches(it) }?.mac ?: emptyList() + val onSaveHost: (KnownHost) -> Unit = { updated -> + knownHostStore.update(kh.address, kh.port, updated) + savedHosts = knownHostStore.all() + editTarget = null + } + if (gamepadUi) { + // Console edit: the same field list + on-screen keyboard as Add-Host, seeded from the + // host with an extra MAC row; the action SAVES instead of connecting. + GamepadAddHostScreen( + onAdd = { _, _, _ -> }, + onDismiss = { editTarget = null }, + editHost = kh, + suggestedMacs = suggested, + onSave = onSaveHost, + ) + } else { + EditHostDialog( + target = kh, + suggestedMacs = suggested, + onSave = onSaveHost, + onDismiss = { editTarget = null }, + ) + } } + + // Topmost: the "Waking…" overlay rides over both the touch grid and the console home. + WakeOverlay(waker, gamepadUi) } /** diff --git a/clients/android/app/src/main/kotlin/io/unom/punktfunk/ControllersScreen.kt b/clients/android/app/src/main/kotlin/io/unom/punktfunk/ControllersScreen.kt index fcbcb62..e567b18 100644 --- a/clients/android/app/src/main/kotlin/io/unom/punktfunk/ControllersScreen.kt +++ b/clients/android/app/src/main/kotlin/io/unom/punktfunk/ControllersScreen.kt @@ -1,6 +1,7 @@ package io.unom.punktfunk import android.hardware.input.InputManager +import android.os.Build import android.os.CombinedVibration import android.os.Handler import android.os.Looper @@ -244,7 +245,7 @@ private fun PadRow(dev: InputDevice, forwarded: Boolean, gamepadSetting: Int) { style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) - val canRumble = dev.vibratorManager.vibratorIds.isNotEmpty() + val canRumble = deviceHasVibrator(dev) if (canRumble) { OutlinedButton(onClick = { testRumble(dev) }) { Text("Test rumble") } } else { @@ -318,11 +319,27 @@ private fun Group(title: String, content: @Composable ColumnScope.() -> Unit) { } } +/** Whether the controller reports a rumble motor — via VibratorManager (API 31+) or the legacy Vibrator. */ +private fun deviceHasVibrator(dev: InputDevice): Boolean = + if (Build.VERSION.SDK_INT >= 31) { + dev.vibratorManager.vibratorIds.isNotEmpty() + } else { + @Suppress("DEPRECATION") + dev.vibrator.hasVibrator() + } + private fun testRumble(dev: InputDevice) { - val vm = dev.vibratorManager - if (vm.vibratorIds.isEmpty()) return runCatching { - vm.vibrate(CombinedVibration.createParallel(VibrationEffect.createOneShot(300, 200))) + if (Build.VERSION.SDK_INT >= 31) { + val vm = dev.vibratorManager + if (vm.vibratorIds.isEmpty()) return + vm.vibrate(CombinedVibration.createParallel(VibrationEffect.createOneShot(300, 200))) + } else { + @Suppress("DEPRECATION") + val v = dev.vibrator + if (!v.hasVibrator()) return + v.vibrate(VibrationEffect.createOneShot(300, 200)) + } } } diff --git a/clients/android/app/src/main/kotlin/io/unom/punktfunk/GamepadAddHostScreen.kt b/clients/android/app/src/main/kotlin/io/unom/punktfunk/GamepadAddHostScreen.kt new file mode 100644 index 0000000..e00a2d6 --- /dev/null +++ b/clients/android/app/src/main/kotlin/io/unom/punktfunk/GamepadAddHostScreen.kt @@ -0,0 +1,467 @@ +package io.unom.punktfunk + +import android.content.res.Configuration +import androidx.activity.compose.BackHandler +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.OutlinedTextField +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.input.KeyboardType +import dev.chrisbanes.haze.HazeState +import dev.chrisbanes.haze.hazeSource +import io.unom.punktfunk.kit.security.KnownHost +import io.unom.punktfunk.kit.security.KnownHostStore +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +// The gamepad-driven "Add Host" screen — the Android mirror of the Apple client's GamepadAddHostView +// + GamepadKeyboard: three field rows (name / address / port) plus an Add action, navigated with the +// vertical focus list; A on a field opens the on-screen keyboard so a host can be registered end to +// end from the couch. One GamepadNavEffect2D owns BOTH modes (list vs keyboard) so they never fight +// over the shared input probes. B peels one layer: close the keyboard, then cancel the screen. + +// Keyboard grid: digits, qwerty letters, hostname/address punctuation, then space / delete / done. +private val KB_CHAR_ROWS = listOf("1234567890", "qwertyuiop", "asdfghjkl-", "zxcvbnm._:") +private const val KB_ACTIONS_ROW = 4 // index of the [space, delete, done] row +private const val KB_ROWS = 5 + +private class Field(val id: String, val label: String, val value: String, val placeholder: String) + +@Composable +fun GamepadAddHostScreen( + onAdd: (name: String, address: String, port: Int) -> Unit, + onDismiss: () -> Unit, + // Non-null → EDIT mode: fields seed from this host, a MAC row is added, and the action SAVES the + // edited record via [onSave] instead of connecting. [suggestedMacs] prefills a not-yet-learned MAC. + editHost: KnownHost? = null, + suggestedMacs: List = emptyList(), + onSave: ((KnownHost) -> Unit)? = null, +) { + val context = LocalContext.current + val isTv = remember { isTvDevice(context) } + val isEdit = editHost != null + val title = if (isEdit) "Edit Host" else "Add Host" + val actionLabel = if (isEdit) "Save" else "Add Host" + var name by remember { mutableStateOf(editHost?.name ?: "") } + var address by remember { mutableStateOf(editHost?.address ?: "") } + var port by remember { mutableStateOf(editHost?.port?.toString() ?: "9777") } + var mac by remember { mutableStateOf(editHost?.mac?.ifEmpty { suggestedMacs }?.joinToString(", ") ?: "") } + val canAdd = address.isNotBlank() && (port.toIntOrNull() ?: 0) > 0 + fun commit() { + if (isEdit && editHost != null && onSave != null) { + onSave( + editHost.copy( + name = name.trim().ifEmpty { editHost.address }, + address = address.trim(), + port = port.toIntOrNull() ?: editHost.port, + mac = KnownHostStore.parseMacs(mac), + ), + ) + } else { + onAdd(name.trim(), address.trim(), port.toIntOrNull() ?: 9777) + } + } + + // On a TV the OS provides a leanback on-screen keyboard for text fields, so use real (focusable) + // text fields + the system IME there. Our controller keyboard is for a phone-with-controller, + // where the phone's own soft keyboard needs a touch a pad can't provide. + if (isTv) { + TvAddHostForm( + title = title, actionLabel = actionLabel, + name = name, onName = { name = it }, + address = address, onAddress = { address = it }, + port = port, onPort = { port = it.filter(Char::isDigit).take(5) }, + mac = if (isEdit) mac else null, onMac = { mac = it }, + canAdd = canAdd, + onAdd = { commit() }, + onDismiss = onDismiss, + ) + return + } + + var focus by remember { mutableIntStateOf(1) } // start on Address + var editing by remember { mutableStateOf(null) } // field id being typed, or null + var kbRow by remember { mutableIntStateOf(1) } + var kbCol by remember { mutableIntStateOf(0) } + val landscape = LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE + val hazeState = remember { HazeState() } + + val fields = buildList { + add(Field("name", "Name", name, "Optional — e.g. Living Room")) + add(Field("address", "Address", address, "IP or hostname")) + add(Field("port", "Port", port, "9777")) + if (isEdit) add(Field("mac", "Wake MAC", mac, "auto-filled when the host is seen")) + } + val actionIndex = fields.size // the Save/Add action sits just after the last field + + fun openKeyboard(id: String) { editing = id; kbRow = 1; kbCol = 0 } + fun closeKeyboard() { editing = null } + fun editField(id: String, transform: (String) -> String) { + when (id) { + "name" -> name = transform(name) + "address" -> address = transform(address) + "port" -> port = transform(port).take(5) + "mac" -> mac = transform(mac) + } + } + fun allowed(id: String, c: Char): Boolean = when (id) { + "port" -> c.isDigit() + "address" -> c != ' ' + else -> true + } + fun activateField() { + if (focus == actionIndex) { + if (canAdd) commit() else { focus = 1; openKeyboard("address") } + } else { + openKeyboard(fields[focus].id) + } + } + fun pressKey() { + val id = editing ?: return + if (kbRow < KB_ACTIONS_ROW) { + val c = KB_CHAR_ROWS[kbRow][kbCol.coerceIn(0, KB_CHAR_ROWS[kbRow].lastIndex)] + if (allowed(id, c)) editField(id) { it + c } + } else when (kbCol) { + 0 -> if (allowed(id, ' ')) editField(id) { "$it " } + 1 -> editField(id) { it.dropLast(1) } + else -> closeKeyboard() + } + } + + BackHandler { if (editing != null) closeKeyboard() else onDismiss() } + GamepadNavEffect2D( + active = true, + onDirection = { dir -> + if (editing == null) { + when (dir) { + NavDir.UP -> if (focus > 0) focus-- + NavDir.DOWN -> if (focus < actionIndex) focus++ + else -> {} + } + } else { + when (dir) { + NavDir.UP -> if (kbRow > 0) { kbRow--; kbCol = kbCol.coerceIn(0, rowCols(kbRow) - 1) } + NavDir.DOWN -> if (kbRow < KB_ROWS - 1) { kbRow++; kbCol = kbCol.coerceIn(0, rowCols(kbRow) - 1) } + NavDir.LEFT -> if (kbCol > 0) kbCol-- + NavDir.RIGHT -> if (kbCol < rowCols(kbRow) - 1) kbCol++ + } + } + }, + onActivate = { if (editing == null) activateField() else pressKey() }, + onTertiary = { if (editing != null) editField(editing!!) { it.dropLast(1) } }, + onSecondary = { if (editing != null) closeKeyboard() }, + ) + + val onFieldClick: (Int) -> Unit = { i -> if (focus == i) activateField() else focus = i } + val onAddClick: () -> Unit = { if (focus == actionIndex) activateField() else focus = actionIndex } + // Tappable (touch escape hatch): the legend doubles as buttons when there's no working controller. + val typeHints = listOf( + PadGlyph.hint('A', "Type") { pressKey() }, + PadGlyph.hint('X', "Delete") { editing?.let { id -> editField(id) { it.dropLast(1) } } }, + PadGlyph.hint('B', "Done") { closeKeyboard() }, + ) + val sideBySide = landscape && editing != null + + Box(Modifier.fillMaxSize()) { + Box(Modifier.fillMaxSize().hazeSource(hazeState)) { + GamepadFormBackground(Modifier.fillMaxSize()) + + if (sideBySide) { + // Landscape + typing: fields and keyboard SIDE BY SIDE so the field being edited stays + // visible (stacked, the keyboard covered the whole short screen). The legend is NOT put + // under the keyboard here — it floats at the same fixed bottom-left spot as everywhere. + Row( + Modifier.fillMaxSize().systemBarsPadding().padding(start = ConsoleEdgeInset, end = 20.dp, top = 8.dp, bottom = 8.dp), + horizontalArrangement = Arrangement.spacedBy(18.dp), + ) { + Column( + Modifier.weight(1f).fillMaxHeight().verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + ConsoleHeader(title, horizontalInset = false) + fields.forEachIndexed { i, f -> FieldRow(f, focused = false, editing = editing == f.id) { onFieldClick(i) } } + AddActionRow(actionLabel, enabled = canAdd, focused = false) { onAddClick() } + Spacer(Modifier.height(64.dp)) // clear the floating legend at bottom-left + } + Column( + Modifier.weight(1.15f).fillMaxHeight().verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + KeyboardGrid(kbRow, kbCol, compact = true) { r, c -> kbRow = r; kbCol = c; pressKey() } + } + } + } else { + // Portrait (or landscape not typing): the FORM SCROLLS so the Add button is never + // compressed by the keyboard; the keyboard sits below it; the legend floats (fixed). + Column(Modifier.fillMaxSize().systemBarsPadding().padding(horizontal = ConsoleEdgeInset)) { + Column( + Modifier.weight(1f).fillMaxWidth().verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + ConsoleHeader(title, horizontalInset = false) + if (editing == null && !landscape) { + Text( + "Hosts on this network appear automatically — add one by address for everything else.", + style = MaterialTheme.typography.bodyMedium, + color = Color.White.copy(alpha = 0.55f), + modifier = Modifier.widthIn(max = 520.dp).padding(bottom = 8.dp), + ) + } + fields.forEachIndexed { i, f -> FieldRow(f, focused = focus == i && editing == null, editing = editing == f.id) { onFieldClick(i) } } + AddActionRow(actionLabel, enabled = canAdd, focused = focus == actionIndex && editing == null) { onAddClick() } + Spacer(Modifier.height(72.dp)) // last field clears the floating legend when scrolled + } + if (editing != null) { + Spacer(Modifier.height(8.dp)) + // The keyboard fills to the bottom; its bottom frame is padded so the fixed + // legend sits OVER that frame (bottom-left corner) rather than in a gap below. + KeyboardGrid(kbRow, kbCol, compact = false, bottomInset = 52.dp) { r, c -> kbRow = r; kbCol = c; pressKey() } + } + } + } + } + + // Floating legend — ALWAYS at the same fixed bottom-start spot (portrait or landscape, keyboard + // open or not), so opening the keyboard never relocates it below the keys. Backdrop-blurred. + Box( + Modifier.align(Alignment.BottomStart) + .then(if (landscape) Modifier else Modifier.systemBarsPadding()) + .padding(ConsoleLegendInset), + ) { + GamepadHintBar( + if (editing != null) { + typeHints + } else { + listOf( + PadGlyph.hint('A', "Select") { activateField() }, + PadGlyph.hint('B', "Cancel", onClick = onDismiss), + ) + }, + hazeState = hazeState, + ) + } + } +} + +/** + * Add-Host on a TV: real focusable text fields + the system (leanback) IME, driven by the OS. No + * custom keyboard or input probes — the native focus engine moves between fields and the Add button, + * and focusing a field pops the OS keyboard. B backs out. + */ +@Composable +private fun TvAddHostForm( + title: String, + actionLabel: String, + name: String, + onName: (String) -> Unit, + address: String, + onAddress: (String) -> Unit, + port: String, + onPort: (String) -> Unit, + mac: String?, // non-null only in edit mode + onMac: (String) -> Unit, + canAdd: Boolean, + onAdd: () -> Unit, + onDismiss: () -> Unit, +) { + BackHandler(onBack = onDismiss) + val firstFocus = remember { FocusRequester() } + Box(Modifier.fillMaxSize()) { + GamepadFormBackground(Modifier.fillMaxSize()) + Column( + Modifier + .fillMaxSize() + .systemBarsPadding() + .padding(horizontal = 56.dp, vertical = 36.dp) + .widthIn(max = 720.dp) + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + Text(title, style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold, color = Color.White) + Text( + "Hosts on this network appear automatically — add one by address for everything else.", + style = MaterialTheme.typography.bodyMedium, + color = Color.White.copy(alpha = 0.55f), + ) + OutlinedTextField( + value = name, onValueChange = onName, singleLine = true, + label = { Text("Name (optional)") }, + modifier = Modifier.fillMaxWidth().focusRequester(firstFocus), + ) + OutlinedTextField( + value = address, onValueChange = onAddress, singleLine = true, + label = { Text("Address") }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri), + modifier = Modifier.fillMaxWidth(), + ) + OutlinedTextField( + value = port, onValueChange = onPort, singleLine = true, + label = { Text("Port") }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + modifier = Modifier.fillMaxWidth(), + ) + if (mac != null) { + OutlinedTextField( + value = mac, onValueChange = onMac, singleLine = true, + label = { Text("Wake-on-LAN MAC") }, + placeholder = { Text("auto-filled when the host is seen") }, + modifier = Modifier.fillMaxWidth(), + ) + } + Button(onClick = onAdd, enabled = canAdd, modifier = Modifier.fillMaxWidth()) { + Text(actionLabel) + } + } + } + LaunchedEffect(Unit) { runCatching { firstFocus.requestFocus() } } +} + +private fun rowCols(row: Int): Int = if (row < KB_ACTIONS_ROW) KB_CHAR_ROWS[row].length else 3 + +@Composable +private fun FieldRow(f: Field, focused: Boolean, editing: Boolean, onClick: () -> Unit) { + val scale by animateFloatAsState(if (focused || editing) 1f else 0.98f, label = "fieldScale") + val shape = RoundedCornerShape(14.dp) + Row( + modifier = Modifier + .fillMaxWidth() + .graphicsLayer { scaleX = scale; scaleY = scale } + .clip(shape) + .background(if (focused || editing) Color(0x336656F2) else Color(0x14FFFFFF)) + .border(1.dp, if (editing) Color(0xB38678F5) else Color.White.copy(alpha = if (focused) 0.28f else 0.06f), shape) + .clickable(interactionSource = remember { MutableInteractionSource() }, indication = null, onClick = onClick) + .padding(horizontal = 16.dp, vertical = 14.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text(f.label, style = MaterialTheme.typography.bodyLarge, fontWeight = FontWeight.SemiBold, color = Color.White) + Spacer(Modifier.weight(1f)) + Text( + f.value.ifEmpty { f.placeholder }, + style = MaterialTheme.typography.bodyMedium.copy(fontFamily = FontFamily.Monospace), + color = if (f.value.isEmpty()) Color.White.copy(alpha = 0.35f) else Color.White, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + if (editing) Text(" |", color = Color(0xFF8678F5)) + } +} + +@Composable +private fun AddActionRow(label: String, enabled: Boolean, focused: Boolean, onClick: () -> Unit) { + val scale by animateFloatAsState(if (focused) 1f else 0.98f, label = "addScale") + val shape = RoundedCornerShape(14.dp) + Box( + modifier = Modifier + .fillMaxWidth() + .graphicsLayer { scaleX = scale; scaleY = scale } + .clip(shape) + .background(if (focused) Color(0x336656F2) else Color(0x14FFFFFF)) + .border(1.dp, Color.White.copy(alpha = if (focused) 0.28f else 0.06f), shape) + .clickable(interactionSource = remember { MutableInteractionSource() }, indication = null, onClick = onClick) + .padding(vertical = 14.dp), + contentAlignment = Alignment.Center, + ) { + Text( + label, + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Bold, + color = if (enabled) Color(0xFF8678F5) else Color.White.copy(alpha = 0.35f), + ) + } +} + +@Composable +private fun KeyboardGrid( + cursorRow: Int, + cursorCol: Int, + compact: Boolean, + bottomInset: Dp = 0.dp, // empty frame at the bottom of the glass for the floating legend to sit over + onKey: (Int, Int) -> Unit, +) { + val shape = RoundedCornerShape(20.dp) + val gap = if (compact) 5.dp else 7.dp + Column( + Modifier + .fillMaxWidth() + .widthIn(max = 640.dp) + .clip(shape) + .background(Color(0x1FFFFFFF)) + .border(1.dp, Color.White.copy(alpha = 0.12f), shape) + .padding(start = 12.dp, end = 12.dp, top = if (compact) 8.dp else 12.dp, bottom = 12.dp + bottomInset), + verticalArrangement = Arrangement.spacedBy(gap), + ) { + KB_CHAR_ROWS.forEachIndexed { r, chars -> + Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(gap)) { + chars.forEachIndexed { c, ch -> + Keycap(ch.toString(), focused = cursorRow == r && cursorCol == c, compact = compact, modifier = Modifier.weight(1f)) { onKey(r, c) } + } + } + } + Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(gap)) { + Keycap("space", focused = cursorRow == KB_ACTIONS_ROW && cursorCol == 0, compact = compact, modifier = Modifier.weight(2f)) { onKey(KB_ACTIONS_ROW, 0) } + Keycap("⌫", focused = cursorRow == KB_ACTIONS_ROW && cursorCol == 1, compact = compact, modifier = Modifier.weight(1f)) { onKey(KB_ACTIONS_ROW, 1) } + Keycap("Done", focused = cursorRow == KB_ACTIONS_ROW && cursorCol == 2, compact = compact, modifier = Modifier.weight(1.5f)) { onKey(KB_ACTIONS_ROW, 2) } + } + } +} + +@Composable +private fun Keycap(label: String, focused: Boolean, compact: Boolean, modifier: Modifier = Modifier, onClick: () -> Unit) { + Box( + modifier = modifier + .height(if (compact) 34.dp else 44.dp) + .clip(RoundedCornerShape(9.dp)) + .background(if (focused) Color(0xFF8678F5) else Color(0x14FFFFFF)) + .clickable(interactionSource = remember { MutableInteractionSource() }, indication = null, onClick = onClick), + contentAlignment = Alignment.Center, + ) { + Text( + label, + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Medium, + color = if (focused) Color.Black else Color.White, + textAlign = TextAlign.Center, + ) + } +} diff --git a/clients/android/app/src/main/kotlin/io/unom/punktfunk/GamepadChrome.kt b/clients/android/app/src/main/kotlin/io/unom/punktfunk/GamepadChrome.kt new file mode 100644 index 0000000..61c8943 --- /dev/null +++ b/clients/android/app/src/main/kotlin/io/unom/punktfunk/GamepadChrome.kt @@ -0,0 +1,345 @@ +package io.unom.punktfunk + +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.SportsEsports +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.BlendMode +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import dev.chrisbanes.haze.HazeState +import dev.chrisbanes.haze.hazeEffect +import kotlin.math.PI +import kotlin.math.cos +import kotlin.math.max +import kotlin.math.sin + +// The console chrome shared by the gamepad-driven screens — the Android mirror of the Apple client's +// GamepadChrome.swift: a slow-drifting violet aurora backdrop, a bottom button-glyph hint bar, and a +// connected-controller status chip. One look across every screen is what makes the console UI read +// as a coherent mode rather than a set of themed pages. + +/** One drifting colour blob of the aurora field. Integer [sx]/[sy] keep the loop seamless at wrap. */ +private class AuroraBlob( + val color: Color, + val baseX: Float, + val baseY: Float, + val driftX: Float, + val driftY: Float, + val sx: Int, + val sy: Int, + val phase: Float, + val radiusFrac: Float, + val alpha: Float, +) + +private val auroraBlobs = listOf( + AuroraBlob(Color(0xFF877AF5), 0.30f, 0.26f, 0.16f, 0.10f, 1, 1, 0.0f, 0.62f, 0.55f), // brand violet + AuroraBlob(Color(0xFF3E33B8), 0.78f, 0.68f, 0.13f, 0.14f, 1, 2, 2.4f, 0.68f, 0.58f), // deep indigo + AuroraBlob(Color(0xFF9E4CCC), 0.16f, 0.82f, 0.12f, 0.09f, 2, 1, 4.1f, 0.52f, 0.42f), // plum + AuroraBlob(Color(0xFF3862DB), 0.72f, 0.14f, 0.10f, 0.08f, 1, 3, 1.2f, 0.48f, 0.40f), // cool blue +) + +/** + * The living console backdrop: soft violet-family blobs drifting over black on slow, seamless loops, + * finished with a centre-pooling vignette and top/bottom legibility scrims. A Compose approximation + * of the Apple client's MeshGradient aurora — same brand family, same "ambience, never content" role. + */ +@Composable +fun GamepadAuroraBackground(modifier: Modifier = Modifier) { + val transition = rememberInfiniteTransition(label = "aurora") + // A full 0..2π sweep over ~96 s; integer per-blob multipliers make sin/cos continuous at the wrap + // so the field never visibly jumps when the animation restarts. + val angle by transition.animateFloat( + initialValue = 0f, + targetValue = (2 * PI).toFloat(), + animationSpec = infiniteRepeatable(tween(96_000, easing = LinearEasing), RepeatMode.Restart), + label = "angle", + ) + Canvas(modifier) { + drawRect(Color.Black) + val span = max(size.width, size.height) + for (b in auroraBlobs) { + val cx = (b.baseX + b.driftX * sin(angle * b.sx + b.phase)) * size.width + val cy = (b.baseY + b.driftY * cos(angle * b.sy + b.phase)) * size.height + val r = span * b.radiusFrac + drawCircle( + brush = Brush.radialGradient( + colors = listOf(b.color.copy(alpha = b.alpha), Color.Transparent), + center = Offset(cx, cy), + radius = r, + ), + center = Offset(cx, cy), + radius = r, + blendMode = BlendMode.Plus, + ) + } + // Cinematic vignette: pool light centre, sink the corners. + drawRect( + Brush.radialGradient( + colors = listOf(Color.Transparent, Color.Black.copy(alpha = 0.44f)), + center = Offset(size.width / 2, size.height / 2), + radius = span * 0.92f, + ), + ) + // Top/bottom legibility scrim for the pinned title + hint bar. + drawRect( + Brush.verticalGradient( + 0.0f to Color.Black.copy(alpha = 0.40f), + 0.30f to Color.Black.copy(alpha = 0.05f), + 0.70f to Color.Black.copy(alpha = 0.06f), + 1.0f to Color.Black.copy(alpha = 0.42f), + ), + ) + } +} + +/** + * The calm backdrop for the console FORM screens (settings, add-host) — deliberately still and quiet + * (unlike the launcher's drifting aurora), a deep indigo base with two soft brand glows so the glass + * rows have some colour + luminance to sit on. Mirrors the Apple client's GamepadFormBackground. + */ +@Composable +fun GamepadFormBackground(modifier: Modifier = Modifier) { + Canvas(modifier) { + val span = max(size.width, size.height) + drawRect(Color(0xFF131126)) + drawCircle( + brush = Brush.radialGradient( + colors = listOf(Color(0xE6635AAE), Color.Transparent), + center = Offset(size.width * 0.24f, size.height * 0.12f), + radius = span * 0.7f, + ), + center = Offset(size.width * 0.24f, size.height * 0.12f), + radius = span * 0.7f, + ) + drawCircle( + brush = Brush.radialGradient( + colors = listOf(Color(0xBF343E96), Color.Transparent), + center = Offset(size.width * 0.82f, size.height * 0.9f), + radius = span * 0.7f, + ), + center = Offset(size.width * 0.82f, size.height * 0.9f), + radius = span * 0.7f, + ) + } +} + +/** + * The exact inset every console screen places its floating legend at (bottom-start), so the legend + * sits in the SAME spot across Home / Settings / Add-Host and appears pinned while the content behind + * it cross-fades between screens. + */ +val ConsoleLegendInset = PaddingValues(start = 24.dp, bottom = 24.dp) + +/** The shared horizontal inset for a console screen's heading (matches the legend's left edge). */ +val ConsoleEdgeInset = 24.dp + +/** + * The heading every console screen uses — one style, one inset, so titles line up across Home / + * Settings / Add-Host / Library. Callers place it at the top of their content (or float it, on Home). + */ +@Composable +fun ConsoleHeader(title: String, modifier: Modifier = Modifier, horizontalInset: Boolean = true) { + // `horizontalInset = false` when the caller's container already pads to ConsoleEdgeInset (e.g. a + // LazyColumn contentPadding) — so the heading lands at the SAME 24dp on every screen either way. + val h = if (horizontalInset) ConsoleEdgeInset else 0.dp + Text( + title, + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.Bold, + color = Color.White, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = modifier.padding(start = h, end = h, top = 18.dp, bottom = 10.dp), + ) +} + +/** + * One glyph + label cell of a hint bar. [glyph] is the face letter; [color] its Xbox-convention hue. + * [onClick], when set, makes the cell tappable — a TOUCH escape hatch so a user without a working + * controller can still drive the console UI (and reach Settings to switch it off). + */ +class GamepadHint( + val glyph: Char, + val color: Color, + val text: String, + val onClick: (() -> Unit)? = null, + // Render as the D-pad-centre "select" button (a ring) instead of a lettered face-button disc — + // for a TV remote, which has no A/B/X/Y. + val select: Boolean = false, + // Render as the gamepad Select/View button (a small capsule). + val viewButton: Boolean = false, +) + +/** Xbox-convention face-button colours, so the glyphs read at a glance across the room. */ +object PadGlyph { + val A = Color(0xFF6BBE45) + val B = Color(0xFFD14B4B) + val X = Color(0xFF4B7BD1) + val Y = Color(0xFFE0B23C) + fun hint(glyph: Char, text: String, onClick: (() -> Unit)? = null) = GamepadHint( + glyph, when (glyph) { 'A' -> A; 'B' -> B; 'X' -> X; 'Y' -> Y; else -> Color(0xFF9A93C7) }, text, onClick, + ) +} + +/** A round face-button badge: a coloured disc with the button letter, like a controller's face. */ +@Composable +fun GamepadButtonGlyph(glyph: Char, color: Color, size: androidx.compose.ui.unit.Dp = 26.dp) { + Box( + modifier = Modifier + .size(size) + .clip(CircleShape) + .background(color), + contentAlignment = Alignment.Center, + ) { + Text( + glyph.toString(), + color = Color.White, + fontWeight = FontWeight.Bold, + fontSize = (size.value * 0.52f).sp, + textAlign = TextAlign.Center, + ) + } +} + +/** The D-pad-centre "select" button — a green (confirm) disc with a ring; the TV-remote glyph for A. */ +@Composable +private fun SelectGlyph(size: androidx.compose.ui.unit.Dp = 26.dp) { + Box( + modifier = Modifier.size(size).clip(CircleShape).background(PadGlyph.A), + contentAlignment = Alignment.Center, + ) { + Box(Modifier.size(size * 0.46f).clip(CircleShape).border(2.dp, Color.White, CircleShape)) + } +} + +/** The remote's "Back" button — a back-arrow disc; the TV-remote glyph for B (back / cancel / done). */ +@Composable +private fun BackGlyph(size: androidx.compose.ui.unit.Dp = 26.dp) { + GamepadButtonGlyph('↩', PadGlyph.B, size) +} + +/** The gamepad "Select / View" button — a small capsule outline, matching its physical shape. */ +@Composable +private fun ViewButtonGlyph(size: androidx.compose.ui.unit.Dp = 26.dp) { + Box(Modifier.size(size), contentAlignment = Alignment.Center) { + Box( + Modifier + .size(width = size * 0.74f, height = size * 0.46f) + .clip(RoundedCornerShape(50)) + .border(1.6.dp, Color.White.copy(alpha = 0.85f), RoundedCornerShape(50)), + ) + } +} + +/** + * The pinned controls legend every gamepad screen shows along the bottom — worn as a self-contained + * translucent pill so it floats over the aurora rather than dissolving into it. + */ +@Composable +fun GamepadHintBar(hints: List, modifier: Modifier = Modifier, hazeState: HazeState? = null) { + // On a TV D-pad remote (no A/B/X/Y), auto-swap the two universal pad glyphs every screen uses: + // A (confirm) → the select ring, B (back/cancel) → a back glyph. Screen-specific glyphs like the + // home's Up/Down handle themselves. Defaults to the gamepad look off an Activity (preview/tests). + val padIsGamepad = (LocalContext.current as? MainActivity)?.lastPadIsGamepad ?: true + val shape = RoundedCornerShape(50) + // With a haze source, blur the content behind the pill (real backdrop blur, API 31+; a translucent + // scrim below) + a light tint; otherwise fall back to a solid frosted fill. + val frosted = if (hazeState != null) { + modifier.clip(shape).hazeEffect(hazeState).background(Color(0x4014122A)) + } else { + modifier.clip(shape).background(Color(0x8C14122A)) + } + Row( + modifier = frosted + .border(1.dp, Color.White.copy(alpha = 0.14f), shape) + .padding(horizontal = 16.dp, vertical = 10.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(11.dp), + ) { + for (h in hints) { + val cb = h.onClick + val cell = if (cb != null) { + Modifier.clip(RoundedCornerShape(50)).clickable(onClick = cb).padding(horizontal = 4.dp, vertical = 5.dp) + } else { + Modifier + } + Row(modifier = cell, verticalAlignment = Alignment.CenterVertically) { + when { + h.viewButton -> ViewButtonGlyph() + h.select || (!padIsGamepad && h.glyph == 'A') -> SelectGlyph() + !padIsGamepad && h.glyph == 'B' -> BackGlyph() + else -> GamepadButtonGlyph(h.glyph, h.color) + } + Spacer(Modifier.width(6.dp)) + Text( + h.text, + style = MaterialTheme.typography.labelLarge, + color = Color.White.copy(alpha = 0.9f), + maxLines = 1, + softWrap = false, // never char-wrap a label when several hints crowd a narrow pill + ) + } + } + } +} + +/** "Which pad is driving this UI" — a quiet chip in the console top bar with the controller's name. */ +@Composable +fun ControllerStatusChip(name: String, modifier: Modifier = Modifier) { + Row( + modifier = modifier + .clip(RoundedCornerShape(50)) + .background(Color.White.copy(alpha = 0.08f)) + .padding(horizontal = 12.dp, vertical = 7.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + Icons.Filled.SportsEsports, + contentDescription = null, + tint = Color.White.copy(alpha = 0.75f), + modifier = Modifier.size(16.dp), + ) + Spacer(Modifier.width(7.dp)) + Text( + name, + style = MaterialTheme.typography.labelMedium, + color = Color.White.copy(alpha = 0.75f), + maxLines = 1, + ) + } +} diff --git a/clients/android/app/src/main/kotlin/io/unom/punktfunk/GamepadDialogs.kt b/clients/android/app/src/main/kotlin/io/unom/punktfunk/GamepadDialogs.kt new file mode 100644 index 0000000..0f87d2c --- /dev/null +++ b/clients/android/app/src/main/kotlin/io/unom/punktfunk/GamepadDialogs.kt @@ -0,0 +1,357 @@ +package io.unom.punktfunk + +import android.os.Build +import androidx.activity.compose.BackHandler +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import io.unom.punktfunk.kit.NativeBridge +import io.unom.punktfunk.kit.security.ClientIdentity +import io.unom.punktfunk.models.PendingTrust +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +// Console-styled trust/pairing dialogs — the controller-navigable counterparts of the touch +// AlertDialogs in ConnectDialogs.kt, shown while the gamepad UI is active. A dark glass card over a +// scrim with focusable action buttons: D-pad left/right moves the focus, A activates it, B dismisses. + +/** One dialog action button. */ +class DialogAction( + val label: String, + val primary: Boolean = false, + val enabled: Boolean = true, + val onClick: () -> Unit, +) + +/** + * The shared console-dialog scaffold: scrim + glass card with a title, [body], and a row of focusable + * [actions]. Owns its own controller nav (the presenting carousel drops its probes while a dialog is + * up, via ConnectScreen's `navActive`). B → [onDismiss]. + */ +@Composable +fun GamepadDialog( + title: String, + onDismiss: () -> Unit, + actions: List, + body: @Composable ColumnScope.() -> Unit, +) { + // Focus the primary action; buttons are stacked full-width, navigated up/down (fits long labels + // like "Request access" without the cramped-row wrapping a horizontal layout caused). + var focus by remember { mutableIntStateOf(actions.indexOfFirst { it.primary }.coerceAtLeast(0)) } + BackHandler(onBack = onDismiss) + GamepadNavEffect2D( + active = true, + onDirection = { dir -> + when (dir) { + NavDir.UP -> if (focus > 0) focus-- + NavDir.DOWN -> if (focus < actions.lastIndex) focus++ + else -> {} + } + }, + onActivate = { actions.getOrNull(focus)?.takeIf { it.enabled }?.onClick?.invoke() }, + ) + // Cap the card to most of the screen and let the BODY scroll — in a short landscape window the + // title + body + buttons would otherwise overflow and compress/clip the bottom button. + val maxCardHeight = (LocalConfiguration.current.screenHeightDp * 0.92f).dp + Box( + Modifier.fillMaxSize().background(Color.Black.copy(alpha = 0.62f)), + contentAlignment = Alignment.Center, + ) { + Column( + Modifier + .padding(24.dp) + .widthIn(max = 520.dp) + .heightIn(max = maxCardHeight) + .clip(RoundedCornerShape(24.dp)) + .background(Color(0xF01A1730)) + .border(1.dp, Color.White.copy(alpha = 0.12f), RoundedCornerShape(24.dp)) + .padding(28.dp), + verticalArrangement = Arrangement.spacedBy(14.dp), + ) { + Text(title, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold, color = Color.White) + // The body scrolls; the title above and the buttons below stay pinned + always visible. + Column( + Modifier.weight(1f, fill = false).verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + body() + } + Spacer(Modifier.size(4.dp)) + actions.forEachIndexed { i, a -> + DialogButton(a.label, focused = i == focus, primary = a.primary, enabled = a.enabled, onClick = a.onClick) + } + } + } +} + +@Composable +private fun DialogButton(label: String, focused: Boolean, primary: Boolean, enabled: Boolean, onClick: () -> Unit) { + val scale by animateFloatAsState(if (focused) 1.02f else 1f, label = "btnScale") + val shape = RoundedCornerShape(14.dp) + val bg = when { + focused -> Color(0xFF6656F2) + primary -> Color(0x336656F2) + else -> Color(0x14FFFFFF) + } + val fg = when { + !enabled -> Color.White.copy(alpha = 0.35f) + focused -> Color.White + primary -> Color(0xFF8678F5) + else -> Color.White.copy(alpha = 0.85f) + } + Box( + modifier = Modifier + .fillMaxWidth() + .graphicsLayer { scaleX = scale; scaleY = scale } + .clip(shape) + .background(bg) + .border(1.dp, Color.White.copy(alpha = if (focused) 0.3f else 0.08f), shape) + .clickable( + enabled = enabled, + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = onClick, + ) + .padding(horizontal = 20.dp, vertical = 13.dp), + contentAlignment = Alignment.Center, + ) { + Text(label, style = MaterialTheme.typography.labelLarge, fontWeight = FontWeight.SemiBold, color = fg, maxLines = 1) + } +} + +/** Body text helper — a dimmed paragraph. */ +@Composable +private fun DialogText(text: String) { + Text(text, style = MaterialTheme.typography.bodyMedium, color = Color.White.copy(alpha = 0.7f)) +} + +/** + * Console host options for a saved tile — Wake (offered only when offline + a MAC is known), Edit, + * Forget. Reached by pressing Up on a focused saved host in the carousel; the console counterpart of + * the touch host card's overflow menu. + */ +@Composable +fun GamepadHostOptionsDialog( + hostName: String, + canWake: Boolean, + onWake: () -> Unit, + onLibrary: (() -> Unit)?, // non-null when the game library is enabled → reachable without Y + onEdit: () -> Unit, + onForget: () -> Unit, + onDismiss: () -> Unit, +) { + GamepadDialog( + title = hostName, + onDismiss = onDismiss, + actions = buildList { + if (onLibrary != null) add(DialogAction("Library", primary = true, onClick = onLibrary)) + if (canWake) add(DialogAction("Wake host", onClick = onWake)) + add(DialogAction("Edit…", primary = onLibrary == null, onClick = onEdit)) + add(DialogAction("Forget", onClick = onForget)) + add(DialogAction("Cancel", onClick = onDismiss)) + }, + ) { + DialogText("Manage this saved host.") + } +} + +@Composable +fun GamepadTrustNewDialog(pt: PendingTrust, onTrust: () -> Unit, onPairInstead: () -> Unit, onDismiss: () -> Unit) { + GamepadDialog( + title = "Trust this host?", + onDismiss = onDismiss, + actions = listOf( + DialogAction("Cancel", onClick = onDismiss), + DialogAction("Pair with PIN", onClick = onPairInstead), + DialogAction("Trust (TOFU)", primary = true, onClick = onTrust), + ), + ) { + DialogText("First connection to ${pt.host}:${pt.port}.") + pt.advertisedFp?.let { DialogText("Fingerprint ${it.take(16)}…") } + DialogText( + "This host allows trust-on-first-use, but that can't tell an impostor from the real host. " + + "Pairing with a PIN is stronger — it proves both sides.", + ) + } +} + +@Composable +fun GamepadFingerprintChangedDialog(pt: PendingTrust, onRepair: () -> Unit, onDismiss: () -> Unit) { + GamepadDialog( + title = "Host identity changed", + onDismiss = onDismiss, + actions = listOf( + DialogAction("Cancel", onClick = onDismiss), + DialogAction("Re-pair", primary = true, onClick = onRepair), + ), + ) { + DialogText( + "The pinned fingerprint for ${pt.host} no longer matches what it now advertises. This can " + + "mean a host reinstall — or an impostor. Re-pair with the host's PIN to continue.", + ) + } +} + +@Composable +fun GamepadRequestAccessDialog(pt: PendingTrust, onRequestAccess: () -> Unit, onUsePin: () -> Unit, onDismiss: () -> Unit) { + GamepadDialog( + title = "Pairing required", + onDismiss = onDismiss, + actions = listOf( + DialogAction("Cancel", onClick = onDismiss), + DialogAction("Use a PIN", onClick = onUsePin), + DialogAction("Request access", primary = true, onClick = onRequestAccess), + ), + ) { + DialogText("${pt.host}:${pt.port} requires pairing before it will stream.") + DialogText( + "Request access and approve this device in the host's console (or web UI) — no PIN needed. " + + "Or pair with the 4-digit PIN the host displays.", + ) + } +} + +@Composable +fun GamepadAwaitingApprovalDialog(hostLabel: String, onCancel: () -> Unit) { + GamepadDialog( + title = "Waiting for approval", + onDismiss = onCancel, + actions = listOf(DialogAction("Cancel", primary = true, onClick = onCancel)), + ) { + val deviceName = Build.MODEL ?: "this device" + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) { + CircularProgressIndicator(modifier = Modifier.size(20.dp), strokeWidth = 2.dp, color = Color.White) + Text("Approve this device on $hostLabel.", color = Color.White) + } + DialogText( + "Open the host's console (or web UI) and approve “$deviceName”. It connects automatically " + + "once you approve — no PIN needed.", + ) + } +} + +/** + * Console PIN pairing: four digit slots set with the D-pad (left/right selects a slot, up/down changes + * 0–9), then Pair. Runs [NativeBridge.nativePair] off the UI thread; on success hands the verified + * fingerprint to [onPaired]. No text keyboard needed — a PIN is four digits. + */ +@Composable +fun GamepadPairPinDialog(pt: PendingTrust, identity: ClientIdentity?, onPaired: (String) -> Unit, onDismiss: () -> Unit) { + val scope = rememberCoroutineScope() + val digits = remember(pt) { mutableStateListOf(0, 0, 0, 0) } + var slot by remember(pt) { mutableIntStateOf(0) } // 0..3 = digit slots, 4 = Pair button + var pairing by remember(pt) { mutableStateOf(false) } + var err by remember(pt) { mutableStateOf(null) } + val name = remember { Build.MODEL ?: "Android" } + + fun pair() { + val id = identity ?: return + pairing = true + err = null + val pin = digits.joinToString("") + scope.launch { + val fp = withContext(Dispatchers.IO) { + NativeBridge.nativePair(pt.host, pt.port, id.certPem, id.privateKeyPem, pin, name) + } + pairing = false + if (fp.isNotEmpty()) onPaired(fp) else err = "Pairing failed — wrong PIN, or the host isn't armed." + } + } + + BackHandler(onBack = { if (!pairing) onDismiss() }) + GamepadNavEffect2D( + active = !pairing, + onDirection = { dir -> + when (dir) { + NavDir.LEFT -> if (slot > 0) slot-- + NavDir.RIGHT -> if (slot < 4) slot++ + NavDir.UP -> if (slot < 4) digits[slot] = (digits[slot] + 1) % 10 + NavDir.DOWN -> if (slot < 4) digits[slot] = (digits[slot] + 9) % 10 + } + }, + onActivate = { if (slot == 4 && identity != null) pair() }, + ) + + val maxCardHeight = (LocalConfiguration.current.screenHeightDp * 0.92f).dp + Box(Modifier.fillMaxSize().background(Color.Black.copy(alpha = 0.62f)), contentAlignment = Alignment.Center) { + Column( + Modifier.padding(24.dp).widthIn(max = 460.dp).heightIn(max = maxCardHeight) + .clip(RoundedCornerShape(24.dp)) + .background(Color(0xF01A1730)).border(1.dp, Color.White.copy(alpha = 0.12f), RoundedCornerShape(24.dp)) + .verticalScroll(rememberScrollState()) + .padding(28.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(18.dp), + ) { + Text("Pair with PIN", style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold, color = Color.White) + Text( + "Enter the 4-digit PIN shown on the host — D-pad ↑↓ sets a digit, ←→ moves.", + style = MaterialTheme.typography.bodyMedium, color = Color.White.copy(alpha = 0.7f), textAlign = TextAlign.Center, + ) + Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { + repeat(4) { i -> PinSlot(digits[i], focused = slot == i && !pairing) } + } + err?.let { Text(it, color = Color(0xFFE0736F), style = MaterialTheme.typography.bodyMedium) } + DialogButton( + label = if (pairing) "Pairing…" else "Pair", + focused = slot == 4 && !pairing, + primary = true, + enabled = !pairing && identity != null, + onClick = { if (identity != null) pair() }, + ) + } + } +} + +@Composable +private fun PinSlot(value: Int, focused: Boolean) { + val shape = RoundedCornerShape(12.dp) + Box( + Modifier.size(54.dp, 66.dp).clip(shape) + .background(if (focused) Color(0x336656F2) else Color(0x14FFFFFF)) + .border(if (focused) 2.dp else 1.dp, if (focused) Color(0xFF8678F5) else Color.White.copy(alpha = 0.1f), shape), + contentAlignment = Alignment.Center, + ) { + Text(value.toString(), fontSize = 30.sp, fontWeight = FontWeight.Bold, color = Color.White, fontFamily = FontFamily.Monospace) + } +} diff --git a/clients/android/app/src/main/kotlin/io/unom/punktfunk/GamepadHome.kt b/clients/android/app/src/main/kotlin/io/unom/punktfunk/GamepadHome.kt new file mode 100644 index 0000000..15013e6 --- /dev/null +++ b/clients/android/app/src/main/kotlin/io/unom/punktfunk/GamepadHome.kt @@ -0,0 +1,328 @@ +package io.unom.punktfunk + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.PageSize +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Lock +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.BlurredEdgeTreatment +import androidx.compose.ui.draw.blur +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.lerp +import dev.chrisbanes.haze.HazeState +import dev.chrisbanes.haze.hazeSource +import io.unom.punktfunk.kit.security.KnownHost +import kotlin.math.absoluteValue +import kotlinx.coroutines.launch + +// The gamepad-driven home — the Android mirror of the Apple client's GamepadHomeView: a distinct, +// "10-foot" console-style host launcher shown INSTEAD of the touch grid while the console UI is +// active. A center-snapping carousel of hosts (saved first, then discovered, then a trailing Add +// Host tile), driven from the couch: A connects, X opens Settings, Y opens a saved host's library. + +/** One navigable launcher tile — a saved host, a discovered-but-unsaved host, or the Add Host action. */ +class HomeTile( + val id: String, + val title: String, + val subtitle: String, + val filled: Boolean = false, // saved (solid monogram) vs discovered / action (tinted outline) + val online: Boolean = false, // advertising on the LAN right now + val paired: Boolean = false, // pinned identity (shows a lock) + val connecting: Boolean = false, + val isAdd: Boolean = false, // the trailing Add Host tile (plus icon, not a monogram) + val knownHost: KnownHost? = null, // set for saved hosts → enables the library (Y) + val activate: () -> Unit, +) { + // Any SAVED host offers the library (matches Apple) — the fetch itself returns a clear "pair + // first" message if the host hasn't authorized this device for its management API. + val hasLibrary: Boolean get() = knownHost != null +} + +/** + * The console home. [tiles] is rebuilt by the caller from the live host stores; [onActivate] runs a + * tile's action, [onOpenLibrary]/[onOpenSettings] are the Y/X actions. Fully driven by D-pad / stick + * / face buttons (MainActivity already maps a pad's A→center, B→back, sticks→D-pad) and by touch. + */ +@Composable +fun GamepadHome( + tiles: List, + libraryEnabled: Boolean, + controllerName: String?, + // False while a sheet/dialog is on top → the carousel stops consuming the pad so the overlay + // can be driven instead. + navActive: Boolean, + onActivate: (HomeTile) -> Unit, + onOpenLibrary: (HomeTile) -> Unit, + onOpenSettings: () -> Unit, + // Up on a saved host opens its options (Wake / Edit / Forget). Only saved tiles carry a knownHost. + onOptions: (HomeTile) -> Unit = {}, +) { + // Equal inset for the pinned title + hint bar, measured from the safe-area edges (so the legend + // sits the same distance from the left and the bottom). + val landscape = LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE + + val pagerState = rememberPagerState(pageCount = { tiles.size }) + val scope = rememberCoroutineScope() + // navTarget is the navigation authority — a controller move steps THIS, and the pager is pointed + // at it, so a fast repeat coalesces to the latest target instead of reading a lagging currentPage + // mid-animation (which is what let a flick overshoot by two). + var navTarget by remember { mutableStateOf(0) } + LaunchedEffect(pagerState.settledPage) { navTarget = pagerState.settledPage } + val current = tiles.getOrNull(navTarget) + + GamepadNavEffect( + active = navActive && tiles.isNotEmpty(), + onMove = { dir -> + val target = (navTarget + dir).coerceIn(0, tiles.lastIndex) + if (target != navTarget) { + navTarget = target + scope.launch { pagerState.animateScrollToPage(target) } + } + }, + onActivate = { tiles.getOrNull(navTarget)?.let(onActivate) }, // A / D-pad-center → Connect + onSecondary = { // Y (gamepad) → Library + tiles.getOrNull(navTarget)?.takeIf { libraryEnabled && it.hasLibrary }?.let(onOpenLibrary) + }, + onTertiary = onOpenSettings, // X (gamepad) → Settings + // A TV remote has no A/B/X/Y: Up → Settings, Down → a saved host's Options (Wake / Library / + // Edit / Forget). A gamepad instead opens Options on its Select/View button. + onUp = onOpenSettings, + onDown = { tiles.getOrNull(navTarget)?.takeIf { it.knownHost != null }?.let(onOptions) }, + onOptions = { tiles.getOrNull(navTarget)?.takeIf { it.knownHost != null }?.let(onOptions) }, + ) + + // The legend follows the LAST-USED input: a real gamepad shows its A/X/Y face buttons + the + // Select/View button for Options; a TV D-pad remote (no face buttons) shows a select ring + Up + // (Settings) / Down (Options) arrows, with Library folded into Options. Input is universal either + // way. Each hint is also TAPPABLE (touch hatch). + val padIsGamepad = (LocalContext.current as? MainActivity)?.lastPadIsGamepad ?: false + val connectLabel = if (current?.isAdd == true) "Add Host" else "Connect" + val connectAction: () -> Unit = { tiles.getOrNull(navTarget)?.let(onActivate) } + val optionsAction: () -> Unit = { current?.let(onOptions) } + val arrowTint = Color(0xFF9A93C7) + val hints = buildList { + if (padIsGamepad) { + add(PadGlyph.hint('A', connectLabel, onClick = connectAction)) + if (libraryEnabled && current?.hasLibrary == true) add(PadGlyph.hint('Y', "Library") { + tiles.getOrNull(navTarget)?.takeIf { it.hasLibrary }?.let(onOpenLibrary) + }) + add(PadGlyph.hint('X', "Settings", onClick = onOpenSettings)) + // The pad's Select/View button (drawn as its capsule glyph) opens host options. + if (current?.knownHost != null) add(GamepadHint(' ', arrowTint, "Options", onClick = optionsAction, viewButton = true)) + } else { + add(GamepadHint(' ', PadGlyph.A, connectLabel, onClick = connectAction, select = true)) + add(GamepadHint('↑', arrowTint, "Settings", onClick = { onOpenSettings() })) + if (current?.knownHost != null) add(GamepadHint('↓', arrowTint, "Options", onClick = optionsAction)) + } + } + + val hazeState = remember { HazeState() } + + Box(Modifier.fillMaxSize()) { + // The whole backdrop (aurora + carousel) is the haze source, so the floating legend can blur + // whatever scrolls under it. + BoxWithConstraints(Modifier.fillMaxSize().hazeSource(hazeState)) { + GamepadAuroraBackground(Modifier.fillMaxSize()) + + // Carousel centred on the FULL screen — the title + legend FLOAT over it (below), so they + // no longer push the cards below the true centre. + val cardWidth = (maxWidth * 0.82f).coerceAtMost(360.dp) + val cardHeight = (maxHeight * 0.56f).coerceAtMost(216.dp) + val sidePad = ((maxWidth - cardWidth) / 2).coerceAtLeast(0.dp) + Box(Modifier.fillMaxSize().systemBarsPadding()) { + HorizontalPager( + state = pagerState, + pageSize = PageSize.Fixed(cardWidth), + contentPadding = PaddingValues(horizontal = sidePad), + pageSpacing = 22.dp, + modifier = Modifier.fillMaxSize(), + verticalAlignment = Alignment.CenterVertically, + ) { page -> + val tile = tiles[page] + // Real distance-from-centered (page + fractional drag), so the pop tracks the + // live scroll: centered tile at full scale/brightness, neighbours recede + blur. + val offset = ((pagerState.currentPage - page) + pagerState.currentPageOffsetFraction) + .absoluteValue.coerceIn(0f, 1f) + GamepadHostTile( + tile = tile, + modifier = Modifier + .graphicsLayer { + val s = lerp(1f, 0.86f, offset) + scaleX = s + scaleY = s + alpha = lerp(1f, 0.5f, offset) + } + // Unbounded so the depth blur isn't hard-clipped at the card's rectangle + // (the cut-off edge). No-op below API 31; a soft blur above. + .blur(radius = (offset * 12f).dp, edgeTreatment = BlurredEdgeTreatment.Unbounded) + .height(cardHeight) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + ) { + if (page == navTarget) { + onActivate(tile) + } else { + navTarget = page + scope.launch { pagerState.animateScrollToPage(page) } + } + }, + ) + } + } + } + + // Title floats over the top (out of the carousel's layout, so the cards stay centred). Uses + // the shared ConsoleHeader so it lines up with every other screen's heading. + Row( + Modifier.align(Alignment.TopStart).fillMaxWidth().systemBarsPadding() + .padding(end = ConsoleEdgeInset), + verticalAlignment = Alignment.CenterVertically, + ) { + ConsoleHeader("Select a Host", modifier = Modifier.weight(1f)) + if (controllerName != null) ControllerStatusChip(controllerName) + } + + // Legend floats bottom-start with a real backdrop blur of the content behind it. In LANDSCAPE + // it ignores the safe area (the nav-bar inset made the bottom gap look oversized). + Box( + Modifier + .align(Alignment.BottomStart) + .then(if (landscape) Modifier else Modifier.systemBarsPadding()) + .padding(ConsoleLegendInset), + ) { + GamepadHintBar(hints, hazeState = hazeState) + } + } +} + +/** One dark-glass landscape console tile — bigger and bolder than the touch grid's HostCard. */ +@Composable +private fun GamepadHostTile(tile: HomeTile, modifier: Modifier = Modifier) { + val shape = RoundedCornerShape(26.dp) + val wash = if (tile.filled) { + Brush.verticalGradient(listOf(Color(0x336656F2), Color(0x14100C2A))) + } else { + Brush.verticalGradient(listOf(Color(0x1AFFFFFF), Color(0x0DFFFFFF))) + } + Column( + modifier = modifier + .fillMaxWidth() + .clip(shape) + .background(wash) + .border(1.dp, Color.White.copy(alpha = 0.16f), shape) + .padding(22.dp), + ) { + Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.Top) { + MonogramBadge(tile) + Spacer(Modifier.weight(1f)) + Row(verticalAlignment = Alignment.CenterVertically) { + if (tile.paired) { + Icon( + Icons.Filled.Lock, + contentDescription = "Paired", + tint = Color.White.copy(alpha = 0.7f), + modifier = Modifier.padding(end = 6.dp).size(15.dp), + ) + } + if (tile.online) { + Box( + Modifier.size(10.dp).clip(androidx.compose.foundation.shape.CircleShape) + .background(Color(0xFF3CD070)), + ) + } + } + } + Spacer(Modifier.weight(1f)) + Text( + tile.title, + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold, + color = Color.White, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + tile.subtitle, + style = MaterialTheme.typography.bodyMedium, + color = Color.White.copy(alpha = 0.55f), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } +} + +@Composable +private fun MonogramBadge(tile: HomeTile) { + val shape = RoundedCornerShape(15.dp) + val fill = if (tile.filled) { + Brush.verticalGradient(listOf(Color(0xFF6656F2), Color(0xFF8678F5))) + } else { + Brush.verticalGradient(listOf(Color(0x296656F2), Color(0x296656F2))) + } + Box( + modifier = Modifier.size(52.dp).clip(shape).background(fill), + contentAlignment = Alignment.Center, + ) { + when { + tile.connecting -> CircularProgressIndicator( + modifier = Modifier.size(24.dp), + strokeWidth = 2.dp, + color = Color.White, + ) + tile.isAdd -> Icon( + Icons.Filled.Add, + contentDescription = null, + tint = if (tile.filled) Color.White else Color(0xFF8678F5), + ) + else -> Text( + tile.title.trim().firstOrNull()?.uppercaseChar()?.toString() ?: "•", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold, + color = if (tile.filled) Color.White else Color(0xFF8678F5), + ) + } + } +} diff --git a/clients/android/app/src/main/kotlin/io/unom/punktfunk/GamepadNav.kt b/clients/android/app/src/main/kotlin/io/unom/punktfunk/GamepadNav.kt new file mode 100644 index 0000000..a9e8994 --- /dev/null +++ b/clients/android/app/src/main/kotlin/io/unom/punktfunk/GamepadNav.kt @@ -0,0 +1,257 @@ +package io.unom.punktfunk + +import android.os.SystemClock +import android.view.InputDevice +import android.view.KeyEvent +import android.view.MotionEvent +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.platform.LocalContext +import kotlin.math.abs +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive + +// Controller navigation for the console carousels (host launcher + library coverflow). It taps the +// SAME MainActivity input probes the Controllers debug screen uses (padMotionProbe / padKeyProbe) so +// it sees the raw analog stick and consumes it BEFORE MainActivity's stick→D-pad focus synthesis — +// which is what made carousel scrolling feel wrong: that path is edge-only (no hold-to-repeat, so a +// held stick did nothing) and a flick could cross the threshold twice (double-move). Here the left +// stick drives discrete moves with hysteresis (fire once when it crosses HIGH; re-arm only after it +// falls back under LOW → a flick is exactly one move) and auto-repeat while held. The caller coalesces +// the moves against a target index so a fast repeat walks smoothly instead of overshooting. + +private const val STICK_HIGH = 0.6f // cross this to commit a move +private const val STICK_LOW = 0.3f // fall back under this to re-arm (hysteresis) +private const val INITIAL_DELAY_MS = 420L // hold this long before the first auto-repeat +private const val REPEAT_MS = 150L // then repeat this often while held + +private class NavInputState { + @Volatile var stickX = 0f + @Volatile var stickY = 0f + @Volatile var hatX = 0f + @Volatile var hatY = 0f + @Volatile var dpadX = 0 + @Volatile var dpadY = 0 + fun reset() { stickX = 0f; stickY = 0f; hatX = 0f; hatY = 0f; dpadX = 0; dpadY = 0 } +} + +/** A committed navigation direction from the stick / D-pad / HAT. */ +enum class NavDir { UP, DOWN, LEFT, RIGHT } + +/** + * Installs controller navigation for a console screen while [active]. [onMove] gets -1 (left) / +1 + * (right) for each committed step; [onActivate] is A / D-pad-center / Enter, [onTertiary] is X, + * [onSecondary] is Y. B and the shoulders fall through to MainActivity (B → its BACK remap → the + * screen's BackHandler). [active] is set false while a sheet/dialog is on top so the carousel stops + * consuming the pad and the overlay can be navigated. + */ +@Composable +fun GamepadNavEffect( + active: Boolean, + onMove: (Int) -> Unit, + onActivate: () -> Unit, + onSecondary: () -> Unit = {}, + onTertiary: () -> Unit = {}, + // D-pad Up (the carousel is horizontal) → e.g. Settings, since a TV remote has no X face button. + onUp: () -> Unit = {}, + onDown: () -> Unit = {}, + // Context/options menu — fired by the gamepad Select/View button OR a long-press of the select/OK + // button (the Android-TV context-menu convention). A short OK press is [onActivate]. + onOptions: () -> Unit = {}, +) { + val activity = LocalContext.current as? MainActivity ?: return + val state = remember { NavInputState() } + // The effects below are keyed on `active` only (they must NOT restart on every recomposition), so + // they'd otherwise capture the FIRST callbacks — closing over a stale `tiles` (fewer hosts than are + // discovered later, which clamped navigation to that old count). rememberUpdatedState keeps the + // long-lived coroutine/probes pointed at the CURRENT callbacks. + val currentOnMove by rememberUpdatedState(onMove) + val currentOnActivate by rememberUpdatedState(onActivate) + val currentOnSecondary by rememberUpdatedState(onSecondary) + val currentOnTertiary by rememberUpdatedState(onTertiary) + val currentOnUp by rememberUpdatedState(onUp) + val currentOnDown by rememberUpdatedState(onDown) + val currentOnOptions by rememberUpdatedState(onOptions) + + DisposableEffect(active) { + // Stable probe refs (see GamepadNavEffect2D) so onDispose only releases the slot if we still + // own it — a cross-fading-out screen mustn't null the incoming screen's probes. + val motionProbe: (MotionEvent) -> Boolean = probe@{ ev -> + if (ev.isFromSource(InputDevice.SOURCE_JOYSTICK) && ev.actionMasked == MotionEvent.ACTION_MOVE) { + state.stickX = ev.getAxisValue(MotionEvent.AXIS_X) + state.hatX = ev.getAxisValue(MotionEvent.AXIS_HAT_X) + return@probe true // consume → MainActivity's stick→D-pad synthesis stays out of it + } + false + } + val keyProbe: (KeyEvent) -> Boolean = probe@{ ev -> + val down = ev.action == KeyEvent.ACTION_DOWN + val edge = down && ev.repeatCount == 0 + when (ev.keyCode) { + KeyEvent.KEYCODE_DPAD_LEFT -> { state.dpadX = if (down) -1 else 0; true } + KeyEvent.KEYCODE_DPAD_RIGHT -> { state.dpadX = if (down) 1 else 0; true } + // TV remote (no face buttons): Up → Settings, Down → a saved host's Options. + KeyEvent.KEYCODE_DPAD_UP -> { if (edge) currentOnUp(); true } + KeyEvent.KEYCODE_DPAD_DOWN -> { if (edge) currentOnDown(); true } + KeyEvent.KEYCODE_BUTTON_A, KeyEvent.KEYCODE_DPAD_CENTER, + KeyEvent.KEYCODE_ENTER, KeyEvent.KEYCODE_NUMPAD_ENTER -> { if (edge) currentOnActivate(); true } + // The gamepad Select / View / Share button → context options (a remote uses Down). + KeyEvent.KEYCODE_BUTTON_SELECT -> { if (edge) currentOnOptions(); true } + KeyEvent.KEYCODE_BUTTON_X -> { if (edge) currentOnTertiary(); true } + KeyEvent.KEYCODE_BUTTON_Y -> { if (edge) currentOnSecondary(); true } + else -> false // B / shoulders / etc. → MainActivity handles (B remaps to BACK) + } + } + if (active) { + activity.padMotionProbe = motionProbe + activity.padKeyProbe = keyProbe + } + onDispose { + if (activity.padMotionProbe === motionProbe) activity.padMotionProbe = null + if (activity.padKeyProbe === keyProbe) activity.padKeyProbe = null + state.reset() + } + } + + LaunchedEffect(active) { + if (!active) return@LaunchedEffect + var committed = 0 // the direction currently held (hysteresis + repeat authority) + var fireAt = 0L // uptime at/after which the next auto-repeat may fire + while (isActive) { + val now = SystemClock.uptimeMillis() + val hat = if (state.hatX <= -0.5f) -1 else if (state.hatX >= 0.5f) 1 else 0 + val dir = when { + state.dpadX != 0 -> state.dpadX + hat != 0 -> hat + else -> { + val x = state.stickX + when { + x >= STICK_HIGH -> 1 + x <= -STICK_HIGH -> -1 + abs(x) < STICK_LOW -> 0 + else -> committed // inside the hysteresis band → hold the committed value + } + } + } + when { + dir == 0 -> committed = 0 + dir != committed -> { currentOnMove(dir); committed = dir; fireAt = now + INITIAL_DELAY_MS } + now >= fireAt -> { currentOnMove(dir); fireAt = now + REPEAT_MS } + } + delay(16) + } + } +} + +/** + * 2-D controller navigation for the console form screens (settings focus list, add-host, on-screen + * keyboard). Same hysteresis + hold-to-repeat as [GamepadNavEffect] but on both axes — the dominant + * stick axis (or the pressed D-pad/HAT) commits a [NavDir], and it re-arms only after the stick + * returns near centre (so a flick is one step). [onActivate] is A / center, [onTertiary] is X, + * [onSecondary] is Y. B is left to MainActivity's BACK remap → the screen's BackHandler (so B "peels + * one layer": close the keyboard, then the screen). + */ +@Composable +fun GamepadNavEffect2D( + active: Boolean, + onDirection: (NavDir) -> Unit, + onActivate: () -> Unit, + onTertiary: () -> Unit = {}, + onSecondary: () -> Unit = {}, +) { + val activity = LocalContext.current as? MainActivity ?: return + val state = remember { NavInputState() } + val currentOnDirection by rememberUpdatedState(onDirection) + val currentOnActivate by rememberUpdatedState(onActivate) + val currentOnTertiary by rememberUpdatedState(onTertiary) + val currentOnSecondary by rememberUpdatedState(onSecondary) + + DisposableEffect(active) { + // Stable probe refs so onDispose only releases the slot if WE still own it — during a + // cross-fade both the outgoing and incoming screen are briefly composed, and the outgoing's + // teardown must not null out the incoming screen's just-installed probes. + val motionProbe: (MotionEvent) -> Boolean = probe@{ ev -> + if (ev.isFromSource(InputDevice.SOURCE_JOYSTICK) && ev.actionMasked == MotionEvent.ACTION_MOVE) { + state.stickX = ev.getAxisValue(MotionEvent.AXIS_X) + state.stickY = ev.getAxisValue(MotionEvent.AXIS_Y) + state.hatX = ev.getAxisValue(MotionEvent.AXIS_HAT_X) + state.hatY = ev.getAxisValue(MotionEvent.AXIS_HAT_Y) + return@probe true + } + false + } + val keyProbe: (KeyEvent) -> Boolean = probe@{ ev -> + val down = ev.action == KeyEvent.ACTION_DOWN + val edge = down && ev.repeatCount == 0 + when (ev.keyCode) { + KeyEvent.KEYCODE_DPAD_LEFT -> { state.dpadX = if (down) -1 else 0; true } + KeyEvent.KEYCODE_DPAD_RIGHT -> { state.dpadX = if (down) 1 else 0; true } + KeyEvent.KEYCODE_DPAD_UP -> { state.dpadY = if (down) -1 else 0; true } + KeyEvent.KEYCODE_DPAD_DOWN -> { state.dpadY = if (down) 1 else 0; true } + KeyEvent.KEYCODE_BUTTON_A, KeyEvent.KEYCODE_DPAD_CENTER, + KeyEvent.KEYCODE_ENTER, KeyEvent.KEYCODE_NUMPAD_ENTER -> { if (edge) currentOnActivate(); true } + KeyEvent.KEYCODE_BUTTON_X -> { if (edge) currentOnTertiary(); true } + KeyEvent.KEYCODE_BUTTON_Y -> { if (edge) currentOnSecondary(); true } + else -> false // B / shoulders → MainActivity (B remaps to BACK → BackHandler) + } + } + if (active) { + activity.padMotionProbe = motionProbe + activity.padKeyProbe = keyProbe + } + onDispose { + if (activity.padMotionProbe === motionProbe) activity.padMotionProbe = null + if (activity.padKeyProbe === keyProbe) activity.padKeyProbe = null + state.reset() + } + } + + LaunchedEffect(active) { + if (!active) return@LaunchedEffect + var committed: NavDir? = null + var fireAt = 0L + while (isActive) { + val now = SystemClock.uptimeMillis() + val raw = resolveDir(state) + val nearCentre = state.dpadX == 0 && state.dpadY == 0 && + abs(state.hatX) < 0.5f && abs(state.hatY) < 0.5f && + abs(state.stickX) < STICK_LOW && abs(state.stickY) < STICK_LOW + when { + raw == null && nearCentre -> committed = null + raw == null -> { /* in the hysteresis band → hold, don't fire */ } + raw != committed -> { currentOnDirection(raw); committed = raw; fireAt = now + INITIAL_DELAY_MS } + now >= fireAt -> { currentOnDirection(raw); fireAt = now + REPEAT_MS } + } + delay(16) + } + } +} + +/** The direction currently past the commit threshold (D-pad/HAT first, then the dominant stick axis). */ +private fun resolveDir(s: NavInputState): NavDir? { + if (s.dpadY < 0) return NavDir.UP + if (s.dpadY > 0) return NavDir.DOWN + if (s.dpadX < 0) return NavDir.LEFT + if (s.dpadX > 0) return NavDir.RIGHT + if (s.hatY <= -0.5f) return NavDir.UP + if (s.hatY >= 0.5f) return NavDir.DOWN + if (s.hatX <= -0.5f) return NavDir.LEFT + if (s.hatX >= 0.5f) return NavDir.RIGHT + return if (abs(s.stickY) >= abs(s.stickX)) { + when { + s.stickY <= -STICK_HIGH -> NavDir.UP + s.stickY >= STICK_HIGH -> NavDir.DOWN + else -> null + } + } else { + when { + s.stickX <= -STICK_HIGH -> NavDir.LEFT + s.stickX >= STICK_HIGH -> NavDir.RIGHT + else -> null + } + } +} diff --git a/clients/android/app/src/main/kotlin/io/unom/punktfunk/GamepadSettingsScreen.kt b/clients/android/app/src/main/kotlin/io/unom/punktfunk/GamepadSettingsScreen.kt new file mode 100644 index 0000000..8ef08b1 --- /dev/null +++ b/clients/android/app/src/main/kotlin/io/unom/punktfunk/GamepadSettingsScreen.kt @@ -0,0 +1,313 @@ +package io.unom.punktfunk + +import android.content.res.Configuration +import androidx.activity.compose.BackHandler +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import dev.chrisbanes.haze.HazeState +import dev.chrisbanes.haze.hazeSource + +// The gamepad-driven settings screen — the Android mirror of the Apple client's GamepadSettingsView: +// the couch-relevant subset of the touch settings restyled as a console page and fully navigable with +// a controller: up/down moves the focus bar, left/right steps the focused value, A cycles/toggles it, +// B closes. Both write the same SharedPreferences, so values round-trip with the touch settings. + +private class GpRow( + val id: String, + val header: String?, + val label: String, + val value: String, + val detail: String, + val adjust: (Int) -> Boolean, // left/right; returns whether the value actually changed + val activate: () -> Unit, // A → cycle forward (wrapping) / flip +) + +@Composable +fun GamepadSettingsScreen( + initial: Settings, + onChange: (Settings) -> Unit, + onBack: () -> Unit, + navActive: Boolean = true, // false while this screen is cross-fading out, so it drops the pad +) { + var s by remember { mutableStateOf(initial) } + fun update(next: Settings) { s = next; onChange(next) } + + val rows = buildSettingsRows(s, ::update) + var focus by remember { mutableIntStateOf(0) } + if (focus > rows.lastIndex) focus = rows.lastIndex + val listState = rememberLazyListState() + + val landscape = LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE + + BackHandler(onBack = onBack) + GamepadNavEffect2D( + active = navActive, + onDirection = { dir -> + when (dir) { + NavDir.UP -> if (focus > 0) focus-- + NavDir.DOWN -> if (focus < rows.lastIndex) focus++ + NavDir.LEFT -> rows.getOrNull(focus)?.adjust(-1) + NavDir.RIGHT -> rows.getOrNull(focus)?.adjust(1) + } + }, + onActivate = { rows.getOrNull(focus)?.activate() }, + ) + // Keep the focused row on screen, but only SCROLL when it's actually off-screen — so entering the + // screen (focus on the first row) leaves the "Settings" heading visible instead of jumping past it. + // +1 accounts for the heading being item 0. + LaunchedEffect(focus) { + runCatching { + val itemIndex = focus + 1 + val info = listState.layoutInfo + val item = info.visibleItemsInfo.firstOrNull { it.index == itemIndex } + val offScreen = item == null || + item.offset < info.viewportStartOffset || + item.offset + item.size > info.viewportEndOffset - 96 // keep clear of the floating legend + if (offScreen) listState.animateScrollToItem(itemIndex) + } + } + + val hazeState = remember { HazeState() } + + Box(Modifier.fillMaxSize()) { + // Everything scrolls — including the heading — so nothing is pinned. Vital in landscape, + // where a fixed title + a fixed detail/legend strip ate most of the (short) height. + Box(Modifier.fillMaxSize().hazeSource(hazeState)) { + GamepadFormBackground(Modifier.fillMaxSize()) + LazyColumn( + state = listState, + modifier = Modifier.fillMaxSize().systemBarsPadding(), + contentPadding = PaddingValues(start = 24.dp, end = 24.dp, top = 8.dp, bottom = 104.dp), + verticalArrangement = Arrangement.spacedBy(6.dp), + ) { + item(key = "__title") { + ConsoleHeader("Settings", horizontalInset = false) + } + itemsIndexed(rows, key = { _, r -> r.id }) { index, row -> + SettingRowView(row, focused = index == focus, onClick = { + if (focus == index) row.activate() else focus = index + }) + } + } + } + + // Floating frosted legend — a real backdrop blur of the rows scrolling behind it (no dedicated + // strip). In landscape it ignores the safe area so it hugs the corner instead of the nav-bar inset. + Box( + Modifier + .align(Alignment.BottomStart) + .then(if (landscape) Modifier else Modifier.systemBarsPadding()) + .padding(ConsoleLegendInset), + ) { + GamepadHintBar( + listOf( + GamepadHint('↔', Color(0xFF9A93C7), "Adjust"), + // Tappable too (touch escape hatch): Change cycles the focused row, Done leaves. + PadGlyph.hint('A', "Change") { rows.getOrNull(focus)?.activate() }, + PadGlyph.hint('B', "Done", onClick = onBack), + ), + hazeState = hazeState, + ) + } + } +} + +@Composable +private fun SettingRowView(row: GpRow, focused: Boolean, onClick: () -> Unit) { + val scale by animateFloatAsState(if (focused) 1f else 0.98f, label = "rowScale") + val shape = RoundedCornerShape(14.dp) + Column { + if (row.header != null) { + Text( + row.header.uppercase(), + style = MaterialTheme.typography.labelMedium, + color = Color.White.copy(alpha = 0.45f), + letterSpacing = 1.4.sp, + modifier = Modifier.padding(start = 16.dp, top = 14.dp, bottom = 4.dp), + ) + } + Column( + modifier = Modifier + .fillMaxWidth() + .graphicsLayer { scaleX = scale; scaleY = scale } + .clip(shape) + .background(if (focused) Color(0x336656F2) else Color(0x14FFFFFF)) + .border(1.dp, Color.White.copy(alpha = if (focused) 0.28f else 0.06f), shape) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = onClick, + ) + .padding(horizontal = 16.dp, vertical = 13.dp), + ) { + Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { + Text( + row.label, + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.SemiBold, + color = Color.White, + maxLines = 1, + ) + Spacer(Modifier.weight(1f)) + if (focused) Text("‹ ", color = Color.White.copy(alpha = 0.6f)) + Text( + row.value, + style = MaterialTheme.typography.bodyMedium, + color = if (focused) Color.White else Color.White.copy(alpha = 0.6f), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + if (focused) Text(" ›", color = Color.White.copy(alpha = 0.6f)) + } + // The focused row carries its own one-line description — no dedicated (space-eating) + // detail strip. It appears right where you're looking, and the row grows to fit. + if (focused && row.detail.isNotBlank()) { + Text( + row.detail, + style = MaterialTheme.typography.bodySmall, + color = Color.White.copy(alpha = 0.6f), + maxLines = 2, + modifier = Modifier.padding(top = 6.dp), + ) + } + } + } +} + +/** Build the console settings rows from the current [Settings], writing through [update]. */ +private fun buildSettingsRows(s: Settings, update: (Settings) -> Unit): List { + fun choice( + id: String, header: String?, label: String, detail: String, + options: List>, current: T, write: (T) -> Unit, + ): GpRow { + val idx = options.indexOfFirst { it.first == current } + return GpRow( + id, header, label, + value = options.getOrNull(idx)?.second ?: "—", + detail = detail, + adjust = { delta -> + if (idx < 0) { + options.firstOrNull()?.let { write(it.first) } != null + } else { + val t = idx + delta + if (t in options.indices) { write(options[t].first); true } else false + } + }, + activate = { + val i = if (idx < 0) 0 else (idx + 1) % options.size + options.getOrNull(i)?.let { write(it.first) } + }, + ) + } + fun toggle( + id: String, header: String?, label: String, detail: String, + value: Boolean, write: (Boolean) -> Unit, + ): GpRow = GpRow( + id, header, label, + value = if (value) "On" else "Off", + detail = detail, + adjust = { delta -> val target = delta > 0; if (value != target) { write(target); true } else false }, + activate = { write(!value) }, + ) + + return listOf( + choice( + "resolution", "Stream", "Resolution", + "The host creates a virtual display at exactly this size — no scaling.", + RESOLUTION_OPTIONS.map { (w, h, lbl) -> (w to h) to lbl }, s.width to s.height, + ) { (w, h) -> update(s.copy(width = w, height = h)) }, + choice( + "refresh", null, "Refresh rate", "Frame rate the host renders and streams at.", + REFRESH_OPTIONS, s.hz, + ) { update(s.copy(hz = it)) }, + choice( + "bitrate", null, "Bitrate", + "Automatic uses the host's default. Run a speed test from the touch UI for an informed value.", + BITRATE_OPTIONS, s.bitrateKbps, + ) { update(s.copy(bitrateKbps = it)) }, + choice( + "compositor", null, "Compositor", + "Which compositor drives the virtual output — honored only if available on the host.", + COMPOSITOR_OPTIONS.mapIndexed { i, lbl -> i to lbl }, s.compositor, + ) { update(s.copy(compositor = it)) }, + + choice( + "codec", "Video", "Video codec", + "A preference — the host falls back if it can't encode this one.", + CODEC_OPTIONS, s.codec, + ) { update(s.copy(codec = it)) }, + toggle( + "hdr", null, "10-bit HDR", + "HDR10 — engages when the host sends HDR content and this display supports it.", + s.hdrEnabled, + ) { update(s.copy(hdrEnabled = it)) }, + + choice( + "audio", "Audio", "Audio channels", "The speaker layout requested from the host.", + AUDIO_CHANNEL_OPTIONS, s.audioChannels, + ) { update(s.copy(audioChannels = it)) }, + toggle( + "mic", null, "Microphone", "Send this device's microphone to the host's virtual mic.", + s.micEnabled, + ) { update(s.copy(micEnabled = it)) }, + + choice( + "padType", "Controller", "Controller type", + "The virtual pad the host creates — Automatic matches this controller.", + GAMEPAD_OPTIONS.mapIndexed { i, lbl -> i to lbl }, s.gamepad, + ) { update(s.copy(gamepad = it)) }, + + toggle( + "hud", "Interface", "Statistics overlay", + "Show FPS, throughput and latency while streaming.", + s.statsHudEnabled, + ) { update(s.copy(statsHudEnabled = it)) }, + toggle( + "library", null, "Game library", + "Browse a paired host's games with Y (experimental).", + s.libraryEnabled, + ) { update(s.copy(libraryEnabled = it)) }, + toggle( + "gamepadUI", null, "Controller-optimized UI", + "Turn off to use the touch interface even with a controller connected.", + s.gamepadUiEnabled, + ) { update(s.copy(gamepadUiEnabled = it)) }, + ) +} diff --git a/clients/android/app/src/main/kotlin/io/unom/punktfunk/GamepadUi.kt b/clients/android/app/src/main/kotlin/io/unom/punktfunk/GamepadUi.kt new file mode 100644 index 0000000..89e73dd --- /dev/null +++ b/clients/android/app/src/main/kotlin/io/unom/punktfunk/GamepadUi.kt @@ -0,0 +1,63 @@ +package io.unom.punktfunk + +import android.app.UiModeManager +import android.content.Context +import android.content.pm.PackageManager +import android.content.res.Configuration +import android.hardware.input.InputManager +import android.os.Handler +import android.os.Looper +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext +import io.unom.punktfunk.kit.Gamepad + +/** + * Whether the controller-optimized "console" home (the host carousel + gamepad chrome) should + * replace the touch UI — the Android mirror of the Apple client's `GamepadUIEnvironment.isActive`: + * the user's [enabled] setting AND (a controller is attached OR this is a TV OR the dev [forced] + * flag). A TV counts unconditionally — its remote/gamepad is the only input, so it's always the + * console UI (as long as the setting is on). + */ +fun gamepadUiActive(enabled: Boolean, controllerConnected: Boolean, tv: Boolean, forced: Boolean): Boolean = + enabled && (controllerConnected || tv || forced) + +/** True on a TV: the leanback/television feature or the TELEVISION ui-mode. */ +fun isTvDevice(context: Context): Boolean { + val pm = context.packageManager + if (pm.hasSystemFeature(PackageManager.FEATURE_LEANBACK) || + pm.hasSystemFeature(PackageManager.FEATURE_TELEVISION) + ) { + return true + } + val uiMode = context.getSystemService(Context.UI_MODE_SERVICE) as? UiModeManager + return uiMode?.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION +} + +/** + * Live "is a game controller attached" state, updated as pads connect/disconnect via + * [InputManager]'s device listener — so the home screen flips to the console UI the instant a pad is + * plugged in or paired, and back to touch when it's removed. Mirrors the reactivity the Apple client + * gets from observing `GamepadManager.shared`. + */ +@Composable +fun rememberControllerConnected(): State { + val context = LocalContext.current + val connected = remember { mutableStateOf(Gamepad.firstPad() != null) } + DisposableEffect(Unit) { + val im = context.getSystemService(Context.INPUT_SERVICE) as InputManager + val listener = object : InputManager.InputDeviceListener { + private fun refresh() { connected.value = Gamepad.firstPad() != null } + override fun onInputDeviceAdded(deviceId: Int) = refresh() + override fun onInputDeviceRemoved(deviceId: Int) = refresh() + override fun onInputDeviceChanged(deviceId: Int) = refresh() + } + im.registerInputDeviceListener(listener, Handler(Looper.getMainLooper())) + connected.value = Gamepad.firstPad() != null + onDispose { im.unregisterInputDeviceListener(listener) } + } + return connected +} diff --git a/clients/android/app/src/main/kotlin/io/unom/punktfunk/LibraryScreen.kt b/clients/android/app/src/main/kotlin/io/unom/punktfunk/LibraryScreen.kt new file mode 100644 index 0000000..9419057 --- /dev/null +++ b/clients/android/app/src/main/kotlin/io/unom/punktfunk/LibraryScreen.kt @@ -0,0 +1,297 @@ +package io.unom.punktfunk + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.PageSize +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.TransformOrigin +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.layout.ContentScale +import android.content.res.Configuration +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.zIndex +import dev.chrisbanes.haze.HazeState +import dev.chrisbanes.haze.hazeSource +import kotlinx.coroutines.launch +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import coil.ImageLoader +import coil.compose.AsyncImage +import coil.request.ImageRequest +import io.unom.punktfunk.kit.library.DEFAULT_MGMT_PORT +import io.unom.punktfunk.kit.library.GameEntry +import io.unom.punktfunk.kit.library.LibraryClient +import io.unom.punktfunk.kit.library.LibraryResult +import io.unom.punktfunk.kit.library.mtlsHttpClient +import io.unom.punktfunk.kit.security.IdentityStore +import io.unom.punktfunk.kit.security.KnownHost +import io.unom.punktfunk.kit.security.obtainIdentity +import kotlin.math.PI +import kotlin.math.absoluteValue +import kotlin.math.cos +import kotlin.math.sign +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +// The host game-library browser — the Android mirror of the Apple client's LibraryCoverflowView: +// a gamepad-driven poster coverflow (centered cover flat + prominent, neighbours receding on a 3D +// Y-tilt) fetched from the host's management API over mTLS. Reached with Y from a saved host. + +private sealed class LibState { + object Loading : LibState() + data class Ready(val games: List, val loader: ImageLoader) : LibState() + data class Message(val text: String) : LibState() // unauthorized / empty / error +} + +@Composable +fun LibraryScreen(host: KnownHost, onBack: () -> Unit, navActive: Boolean = true) { + BackHandler(onBack = onBack) + val context = LocalContext.current + val hazeState = remember { HazeState() } + val landscape = LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE + var state by remember { mutableStateOf(LibState.Loading) } + + LaunchedEffect(host.address, host.port, host.fpHex) { + state = LibState.Loading + state = withContext(Dispatchers.IO) { + val id = runCatching { obtainIdentity(IdentityStore(context)) }.getOrNull() + ?: return@withContext LibState.Message("Identity unavailable — re-pair may be required.") + when (val res = LibraryClient.fetch( + address = host.address, + mgmtPort = DEFAULT_MGMT_PORT, + certPem = id.certPem, + keyPem = id.privateKeyPem, + fpHex = host.fpHex, + )) { + is LibraryResult.Ok -> if (res.games.isEmpty()) { + LibState.Message("No games found on this host.") + } else { + val client = mtlsHttpClient(id.certPem, id.privateKeyPem, host.address, host.fpHex) + LibState.Ready(res.games, ImageLoader.Builder(context).okHttpClient(client).build()) + } + is LibraryResult.Unauthorized -> LibState.Message(res.message) + is LibraryResult.Error -> LibState.Message(res.message) + } + } + } + + Box(Modifier.fillMaxSize()) { + Box(Modifier.fillMaxSize().hazeSource(hazeState)) { + GamepadAuroraBackground(Modifier.fillMaxSize()) + Column(Modifier.fillMaxSize().systemBarsPadding()) { + ConsoleHeader("${host.name} — Library") + Box(Modifier.weight(1f).fillMaxWidth(), contentAlignment = Alignment.Center) { + when (val s = state) { + is LibState.Loading -> LoadingState() + is LibState.Message -> MessageState(s.text) + is LibState.Ready -> Coverflow(s.games, s.loader, navActive) + } + } + } + } + // Floating legend at the shared spot — same landscape-aware inset as every other console + // screen (ignore the safe area in landscape, where the bottom edge isn't a tap target). + Box( + Modifier.align(Alignment.BottomStart) + .then(if (landscape) Modifier else Modifier.systemBarsPadding()) + .padding(ConsoleLegendInset), + ) { + GamepadHintBar(listOf(PadGlyph.hint('B', "Close", onClick = onBack)), hazeState = hazeState) + } + } +} + +@Composable +private fun LoadingState() { + Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(14.dp)) { + CircularProgressIndicator(color = Color.White) + Text("Loading library…", color = Color.White.copy(alpha = 0.7f), style = MaterialTheme.typography.bodyLarge) + } +} + +@Composable +private fun MessageState(text: String) { + Text( + text, + color = Color.White.copy(alpha = 0.75f), + style = MaterialTheme.typography.bodyLarge, + textAlign = TextAlign.Center, + modifier = Modifier.padding(horizontal = 24.dp), + ) +} + +@Composable +private fun Coverflow(games: List, loader: ImageLoader, navActive: Boolean) { + BoxWithConstraints(Modifier.fillMaxSize()) { + // Fit a 2:3 poster into the height the detail line leaves; clamp so it never dwarfs the screen. + val coverHeight = (maxHeight * 0.72f).coerceAtMost(360.dp) + val coverWidth = coverHeight * 2f / 3f + val sidePad = ((maxWidth - coverWidth) / 2).coerceAtLeast(0.dp) + val pagerState = rememberPagerState(pageCount = { games.size }) + val scope = rememberCoroutineScope() + var navTarget by remember { mutableIntStateOf(0) } + LaunchedEffect(pagerState.settledPage) { navTarget = pagerState.settledPage } + val current = games.getOrNull(navTarget) + + // Controller nav: the pad drives the coverflow (it wasn't captured before). Left/right steps a + // coalesced target the pager chases; A is reserved for launch (browse-only for now); B closes + // via the screen's BackHandler. + GamepadNavEffect( + active = navActive && games.isNotEmpty(), + onMove = { dir -> + val t = (navTarget + dir).coerceIn(0, games.lastIndex) + if (t != navTarget) { navTarget = t; scope.launch { pagerState.animateScrollToPage(t) } } + }, + onActivate = { /* launch a title — browse-only for now */ }, + ) + + Column(Modifier.fillMaxSize(), verticalArrangement = Arrangement.Center) { + HorizontalPager( + state = pagerState, + pageSize = PageSize.Fixed(coverWidth), + contentPadding = PaddingValues(horizontal = sidePad), + pageSpacing = 0.dp, // translationX (below) does the spacing so covers sit closer + beyondViewportPageCount = 3, // render more neighbours so a denser fan is visible + modifier = Modifier.fillMaxWidth().height(coverHeight + 24.dp), + verticalAlignment = Alignment.CenterVertically, + ) { page -> + val signed = (pagerState.currentPage - page) + pagerState.currentPageOffsetFraction + val d = signed.absoluteValue + Poster( + game = games[page], + loader = loader, + modifier = Modifier + .zIndex(-d) // centred cover on top, neighbours stacked behind + .width(coverWidth) + .height(coverHeight) + .graphicsLayer { + // Centre at full size; EVERY neighbour settles to one size, so an even pitch + // yields even VISUAL gaps. (A progressive shrink made the outer gaps grow — + // the "edges spread apart while the centre gets crowded" look.) + val scale = 1f - 0.28f * d.coerceAtMost(1f) + scaleX = scale + scaleY = scale + alpha = (1f - 0.26f * d).coerceAtLeast(0.15f) // depth via fade, not size + val rotDeg = signed.coerceIn(-2.5f, 2.5f) * 26f // tilt inward + rotationY = rotDeg + // Even neighbour pitch (0.8·cover) + a little extra outward push (ramped over + // the first step so scrolling stays smooth) so the CENTRE card breathes. + val base = signed * size.width * 0.2f - signed.coerceIn(-1f, 1f) * size.width * 0.14f + // Counter-balance: a rotated card projects narrower (≈cos θ), which opens its + // inner gap — pull it back toward centre by the half-width it loses so the + // gaps stay even no matter the tilt. + val halfW = size.width * scale * 0.5f + val counter = sign(signed) * halfW * (1f - cos(rotDeg * (PI.toFloat() / 180f))) + translationX = base + counter + // Lower cameraDistance = stronger perspective (CSS `perspective`); the flat + // 22 washed the tilt out. 9 makes the same angle read as real depth. + cameraDistance = 9f * density + transformOrigin = TransformOrigin(0.5f, 0.5f) + }, + ) + } + Column( + Modifier.fillMaxWidth().padding(top = 14.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + current?.title ?: " ", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold, + color = Color.White, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + if (current != null) { + Text( + if (current.isCustom) "CUSTOM" else "STEAM", + style = MaterialTheme.typography.labelMedium, + color = Color.White.copy(alpha = 0.5f), + letterSpacing = 2.sp, + ) + } + } + } + } +} + +/** One cover: walks the art candidates (portrait → header → hero) then a text placeholder. */ +@Composable +private fun Poster(game: GameEntry, loader: ImageLoader, modifier: Modifier = Modifier) { + val candidates = game.art.posterCandidates + var idx by remember(game.id) { mutableStateOf(0) } + val shape = RoundedCornerShape(16.dp) + Box( + modifier = modifier + .clip(shape) + .background(Color(0xFF241F3D)) + .border(1.dp, Color.White.copy(alpha = 0.12f), shape), + contentAlignment = Alignment.Center, + ) { + if (idx < candidates.size) { + AsyncImage( + model = ImageRequest.Builder(LocalContext.current).data(candidates[idx]).build(), + imageLoader = loader, + contentDescription = game.title, + contentScale = ContentScale.Crop, + modifier = Modifier.fillMaxSize(), + onError = { idx++ }, // this candidate failed — try the next, or fall to the placeholder + ) + } else { + Text( + game.title, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + color = Color.White.copy(alpha = 0.75f), + textAlign = TextAlign.Center, + modifier = Modifier.padding(12.dp), + ) + } + // Store badge, top-start. + Box(Modifier.fillMaxSize().padding(8.dp), contentAlignment = Alignment.TopStart) { + Text( + if (game.isCustom) "Custom" else "Steam", + style = MaterialTheme.typography.labelSmall, + color = Color.White, + modifier = Modifier + .clip(RoundedCornerShape(50)) + .background(Color.Black.copy(alpha = 0.5f)) + .padding(horizontal = 8.dp, vertical = 3.dp), + ) + } + } +} diff --git a/clients/android/app/src/main/kotlin/io/unom/punktfunk/LicensesScreen.kt b/clients/android/app/src/main/kotlin/io/unom/punktfunk/LicensesScreen.kt index c1dcb45..e6f3ee3 100644 --- a/clients/android/app/src/main/kotlin/io/unom/punktfunk/LicensesScreen.kt +++ b/clients/android/app/src/main/kotlin/io/unom/punktfunk/LicensesScreen.kt @@ -3,14 +3,21 @@ package io.unom.punktfunk import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontFamily @@ -31,6 +38,13 @@ fun LicensesScreen(onBack: () -> Unit) { context.assets.open("THIRD-PARTY-NOTICES.txt").bufferedReader().use { it.readText() } }.getOrDefault("Third-party notices unavailable.") } + // The bundled brand typeface (Geist Sans) ships under the SIL Open Font License 1.1. The OFL + // requires the license travel with the font, so surface it here (mirrors the Apple client). + val fontLicense = remember { + runCatching { + context.assets.open("GEIST-OFL.txt").bufferedReader().use { it.readText() } + }.getOrNull() + } val version = remember { runCatching { @Suppress("DEPRECATION") @@ -38,29 +52,52 @@ fun LicensesScreen(onBack: () -> Unit) { }.getOrNull() } - Column( - modifier = Modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()) - .padding(horizontal = 20.dp, vertical = 24.dp), - verticalArrangement = Arrangement.spacedBy(16.dp), - ) { - Text("Open-source licenses", style = MaterialTheme.typography.headlineMedium) - if (version != null) { - Text( - "punktfunk $version", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) + Column(Modifier.fillMaxSize()) { + // Pinned header with a visible Back affordance (Back-button/gesture still work via BackHandler). + Row( + modifier = Modifier.fillMaxWidth().padding(start = 4.dp, end = 12.dp, top = 8.dp, bottom = 4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + IconButton(onClick = onBack) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back") + } + Text("Open-source licenses", style = MaterialTheme.typography.headlineSmall) + } + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(horizontal = 20.dp) + .padding(bottom = 24.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + if (version != null) { + Text( + "Punktfunk $version", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + Text( + "Punktfunk is licensed under MIT OR Apache-2.0, at your option. It uses the open-source " + + "components below, each under its own license.", + style = MaterialTheme.typography.bodyMedium, + ) + Text( + notices, + style = MaterialTheme.typography.bodySmall.copy(fontFamily = FontFamily.Monospace), + ) + if (fontLicense != null) { + Text("Bundled font", style = MaterialTheme.typography.titleMedium) + Text( + "The Geist typeface is licensed under the SIL Open Font License 1.1.", + style = MaterialTheme.typography.bodyMedium, + ) + Text( + fontLicense, + style = MaterialTheme.typography.bodySmall.copy(fontFamily = FontFamily.Monospace), + ) + } } - Text( - "punktfunk is licensed under MIT OR Apache-2.0, at your option. It uses the open-source " + - "components below, each under its own license.", - style = MaterialTheme.typography.bodyMedium, - ) - Text( - notices, - style = MaterialTheme.typography.bodySmall.copy(fontFamily = FontFamily.Monospace), - ) } } diff --git a/clients/android/app/src/main/kotlin/io/unom/punktfunk/MainActivity.kt b/clients/android/app/src/main/kotlin/io/unom/punktfunk/MainActivity.kt index 3cd7c4e..4beb5fe 100644 --- a/clients/android/app/src/main/kotlin/io/unom/punktfunk/MainActivity.kt +++ b/clients/android/app/src/main/kotlin/io/unom/punktfunk/MainActivity.kt @@ -1,5 +1,6 @@ package io.unom.punktfunk +import android.os.Build import android.os.Bundle import android.view.InputDevice import android.view.KeyEvent @@ -10,6 +11,9 @@ import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.Surface +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import io.unom.punktfunk.kit.Gamepad import io.unom.punktfunk.kit.Keymap @@ -34,8 +38,30 @@ class MainActivity : ComponentActivity() { var padKeyProbe: ((KeyEvent) -> Boolean)? = null var padMotionProbe: ((MotionEvent) -> Boolean)? = null + /** + * Set by [StreamScreen] to its disconnect action. The emergency-exit chord (below) invokes it so a + * couch user with no keyboard/Back can always leave a stream. + */ + var requestStreamExit: (() -> Unit)? = null + + /** Currently-held forwarded pad buttons (bitmask of `Gamepad.BTN_*`), for chord detection. */ + private var heldPadButtons = 0 + + /** + * 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 + * face buttons, or a select glyph + arrows for a remote. Compose observes it (a snapshot state). + */ + var lastPadIsGamepad by mutableStateOf(false) + private set + + /** The panel's highest-refresh display mode (0 = unknown/unsupported), resolved once at startup. */ + private var highRefreshModeId = 0 + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + resolveHighRefreshMode() + 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 // the status/nav bars blend with our surface and get light icons. (The no-arg edge-to-edge // picks the *system* light/dark, which left a black status bar over our dark background.) @@ -43,13 +69,39 @@ class MainActivity : ComponentActivity() { statusBarStyle = SystemBarStyle.dark(android.graphics.Color.TRANSPARENT), navigationBarStyle = SystemBarStyle.dark(android.graphics.Color.TRANSPARENT), ) + // Dev escape hatch (mirrors the Apple client's PUNKTFUNK_FORCE_GAMEPAD_UI): force the console + // UI without a physical pad — `adb shell am start -n io.unom.punktfunk/.MainActivity --ez + // pf_force_gamepad_ui true`. Never set in normal use; real activation is a connected pad / TV. + val forceGamepadUi = intent?.getBooleanExtra("pf_force_gamepad_ui", false) ?: false setContent { PunktfunkTheme { - Surface(modifier = Modifier.fillMaxSize()) { App() } + Surface(modifier = Modifier.fillMaxSize()) { App(forceGamepadUi = forceGamepadUi) } } } } + /** Resolve the panel's highest-refresh mode (same resolution) once, for [setConsoleHighRefreshRate]. */ + private fun resolveHighRefreshMode() { + @Suppress("DEPRECATION") + val disp = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) display else windowManager.defaultDisplay + highRefreshModeId = disp?.supportedModes?.maxWithOrNull( + compareBy({ it.refreshRate }, { it.physicalWidth * it.physicalHeight }), + )?.modeId ?: 0 + } + + /** + * Opt the CONSOLE UI into the panel's highest refresh mode. Some OEMs (Nothing OS among them) pin + * third-party apps to 60Hz unless they explicitly ask for more, which halves the smoothness of the + * UI's scrolling/animation on a 120/144Hz panel. [StreamScreen] turns this OFF while streaming so + * its own `ANativeWindow_setFrameRate` (matched to the video) governs the panel instead. + */ + fun setConsoleHighRefreshRate(high: Boolean) { + if (highRefreshModeId == 0) return + window.attributes = window.attributes.apply { + preferredDisplayModeId = if (high) highRefreshModeId else 0 + } + } + override fun dispatchKeyEvent(event: KeyEvent): Boolean { val handle = streamHandle if (handle != 0L) { @@ -60,9 +112,20 @@ class MainActivity : ComponentActivity() { if (bit != 0) { when (event.action) { // repeatCount guard: don't re-send a held button as auto-repeat. - KeyEvent.ACTION_DOWN -> + KeyEvent.ACTION_DOWN -> { if (event.repeatCount == 0) NativeBridge.nativeSendGamepadButton(handle, bit, true) - KeyEvent.ACTION_UP -> NativeBridge.nativeSendGamepadButton(handle, bit, false) + heldPadButtons = heldPadButtons or bit + // Emergency exit: Select + Start + L1 + R1 held together leaves the stream + // (a couch user has no keyboard/Back). Fired once per full chord. + if (heldPadButtons and STREAM_EXIT_CHORD == STREAM_EXIT_CHORD) { + heldPadButtons = 0 + requestStreamExit?.let { exit -> window.decorView.post { exit() } } + } + } + KeyEvent.ACTION_UP -> { + NativeBridge.nativeSendGamepadButton(handle, bit, false) + heldPadButtons = heldPadButtons and bit.inv() + } } return true // consumed } @@ -90,18 +153,29 @@ class MainActivity : ComponentActivity() { } } } else { + // Note which input the console UI is being driven by, so its glyphs match (a TV remote's + // D-pad is not from SOURCE_GAMEPAD; a pad's face buttons / D-pad are). + if (event.action == KeyEvent.ACTION_DOWN && isConsoleNavKey(event.keyCode)) { + lastPadIsGamepad = event.isFromSource(InputDevice.SOURCE_GAMEPAD) + } // The Controllers debug screen sees pad events before the navigation remap below. padKeyProbe?.let { if (it(event)) return true } if (event.isFromSource(InputDevice.SOURCE_GAMEPAD)) { // Not streaming: a game controller drives the Compose UI (TV + phone). Map the face - // buttons to the navigation keys the focus system understands; D-pad *keys* already - // move focus on their own, so they fall through to super untouched. - val mapped = when (event.keyCode) { - KeyEvent.KEYCODE_BUTTON_A -> KeyEvent.KEYCODE_DPAD_CENTER // activate focused element - KeyEvent.KEYCODE_BUTTON_B -> KeyEvent.KEYCODE_BACK // back / dismiss - else -> 0 + // buttons to the navigation the focus system / back stack understand; D-pad *keys* + // already move focus on their own, so they fall through to super untouched. + when (event.keyCode) { + // B → back. Drive the OnBackPressedDispatcher directly rather than synthesising a + // BACK KeyEvent: a synthetic event isn't "tracking", so the framework's default + // onKeyUp(BACK) never calls onBackPressed() and Compose BackHandlers wouldn't fire. + KeyEvent.KEYCODE_BUTTON_B -> { + if (event.action == KeyEvent.ACTION_UP) onBackPressedDispatcher.onBackPressed() + return true + } + // A → activate the focused element (the focus system understands DPAD_CENTER). + KeyEvent.KEYCODE_BUTTON_A -> + return super.dispatchKeyEvent(KeyEvent(event.action, KeyEvent.KEYCODE_DPAD_CENTER)) } - if (mapped != 0) return super.dispatchKeyEvent(KeyEvent(event.action, mapped)) } } return super.dispatchKeyEvent(event) @@ -137,6 +211,7 @@ class MainActivity : ComponentActivity() { if (dir != lastNavDir) { lastNavDir = dir if (dir != 0) { + lastPadIsGamepad = true // a stick/HAT push can only come from a real gamepad super.dispatchKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, dir)) super.dispatchKeyEvent(KeyEvent(KeyEvent.ACTION_UP, dir)) return true @@ -147,4 +222,17 @@ class MainActivity : ComponentActivity() { } return super.dispatchGenericMotionEvent(event) } + + /** Keys that drive the console UI — D-pad + face buttons; used to classify the last input source. */ + private fun isConsoleNavKey(kc: Int): Boolean = when (kc) { + KeyEvent.KEYCODE_DPAD_UP, KeyEvent.KEYCODE_DPAD_DOWN, KeyEvent.KEYCODE_DPAD_LEFT, + KeyEvent.KEYCODE_DPAD_RIGHT, KeyEvent.KEYCODE_DPAD_CENTER, KeyEvent.KEYCODE_ENTER, + -> true + else -> KeyEvent.isGamepadButton(kc) + } + + private companion object { + /** Emergency stream-exit chord: Select + Start + L1 + R1 held together. */ + val STREAM_EXIT_CHORD = Gamepad.BTN_BACK or Gamepad.BTN_START or Gamepad.BTN_LB or Gamepad.BTN_RB + } } diff --git a/clients/android/app/src/main/kotlin/io/unom/punktfunk/Settings.kt b/clients/android/app/src/main/kotlin/io/unom/punktfunk/Settings.kt index 368bfd1..152c0fc 100644 --- a/clients/android/app/src/main/kotlin/io/unom/punktfunk/Settings.kt +++ b/clients/android/app/src/main/kotlin/io/unom/punktfunk/Settings.kt @@ -41,6 +41,19 @@ data class Settings( * understand touch. Mirrors the Apple client's TouchInputMode. */ val touchMode: TouchMode = TouchMode.TRACKPAD, + /** + * Swap the whole home screen for the controller-optimized "console" UI (the host carousel + + * gamepad chrome) whenever a controller is connected — mirrors the Apple client's + * `gamepadUIEnabled`. On by default; turn it off to keep the touch UI even with a pad attached. + * A TV (leanback) is always in this mode regardless (its remote/pad is the only input). + */ + val gamepadUiEnabled: Boolean = true, + /** + * Show the experimental game-library browser (the coverflow reached with Y from a saved host). + * Fetched from the host's management API over mTLS; needs a paired host. Mirrors the Apple + * client's `libraryEnabled`. + */ + val libraryEnabled: Boolean = true, ) /** [Settings.touchMode] values; persisted by name. */ @@ -67,6 +80,8 @@ class SettingsStore(context: Context) { ?.let { name -> TouchMode.entries.firstOrNull { it.name == name } } // Migration: the pre-enum Boolean "trackpad_mode" (true = trackpad, false = direct). ?: if (prefs.getBoolean(K_TRACKPAD, true)) TouchMode.TRACKPAD else TouchMode.POINTER, + gamepadUiEnabled = prefs.getBoolean(K_GAMEPAD_UI, true), + libraryEnabled = prefs.getBoolean(K_LIBRARY, true), ) fun save(s: Settings) { @@ -83,6 +98,8 @@ class SettingsStore(context: Context) { .putBoolean(K_MIC, s.micEnabled) .putBoolean(K_HUD, s.statsHudEnabled) .putString(K_TOUCH_MODE, s.touchMode.name) + .putBoolean(K_GAMEPAD_UI, s.gamepadUiEnabled) + .putBoolean(K_LIBRARY, s.libraryEnabled) .apply() } @@ -99,6 +116,8 @@ class SettingsStore(context: Context) { const val K_MIC = "mic_enabled" const val K_HUD = "stats_hud_enabled" const val K_TOUCH_MODE = "touch_mode" + const val K_GAMEPAD_UI = "gamepad_ui_enabled" + const val K_LIBRARY = "library_enabled" /** Legacy Boolean the enum replaced — read once as the migration default, never written. */ const val K_TRACKPAD = "trackpad_mode" diff --git a/clients/android/app/src/main/kotlin/io/unom/punktfunk/SettingsScreen.kt b/clients/android/app/src/main/kotlin/io/unom/punktfunk/SettingsScreen.kt index 489819b..351f2e4 100644 --- a/clients/android/app/src/main/kotlin/io/unom/punktfunk/SettingsScreen.kt +++ b/clients/android/app/src/main/kotlin/io/unom/punktfunk/SettingsScreen.kt @@ -5,44 +5,79 @@ import android.content.pm.PackageManager import androidx.activity.compose.BackHandler import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideOutHorizontally +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight +import androidx.compose.material.icons.filled.Info +import androidx.compose.material.icons.filled.SportsEsports +import androidx.compose.material.icons.filled.Tune +import androidx.compose.material.icons.filled.Tv +import androidx.compose.material.icons.filled.VolumeUp import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExposedDropdownMenuBox import androidx.compose.material3.ExposedDropdownMenuAnchorType import androidx.compose.material3.ExposedDropdownMenuDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedCard import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Switch import androidx.compose.material3.Text +import androidx.compose.material3.VerticalDivider import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat /** - * Stream settings, grouped into Display / Host / Audio / Overlay cards. Edits are persisted - * immediately via [onChange]; [onBack] returns to the connect screen. Resolution/refresh "Native" - * resolve from the device display at connect time. + * Stream settings, organised as an iOS-Settings / Android-system-settings style list of category + * subpages. On a phone the category list pushes to a full-screen detail; on a tablet / large screen + * it becomes a two-pane list-detail (the list stays on the left, the detail on the right). Edits + * persist immediately via [onChange]; [onBack] returns to the connect screen. */ @Composable -fun SettingsScreen(initial: Settings, onChange: (Settings) -> Unit, onBack: () -> Unit) { +fun SettingsScreen( + initial: Settings, + onChange: (Settings) -> Unit, + onBack: () -> Unit, +) { var s by remember { mutableStateOf(initial) } val context = LocalContext.current var showLicenses by remember { mutableStateOf(false) } @@ -52,13 +87,20 @@ fun SettingsScreen(initial: Settings, onChange: (Settings) -> Unit, onBack: () - onChange(next) } - BackHandler(onBack = onBack) - // Mic uplink — turning it on requests RECORD_AUDIO; if denied, the toggle stays off. val micLauncher = rememberLauncherForActivityResult( ActivityResultContracts.RequestPermission(), ) { granted -> update(s.copy(micEnabled = granted)) } + val onMicChange: (Boolean) -> Unit = { on -> + when { + !on -> update(s.copy(micEnabled = false)) + ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO) == + PackageManager.PERMISSION_GRANTED -> update(s.copy(micEnabled = true)) + else -> micLauncher.launch(Manifest.permission.RECORD_AUDIO) + } + } + // Deep sub-screens replace the whole settings surface (they carry their own back). if (showLicenses) { LicensesScreen(onBack = { showLicenses = false }) return @@ -68,160 +110,314 @@ fun SettingsScreen(initial: Settings, onChange: (Settings) -> Unit, onBack: () - return } - Column( - modifier = Modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()) - .padding(horizontal = 20.dp, vertical = 24.dp), - verticalArrangement = Arrangement.spacedBy(24.dp), - ) { - Text("Settings", style = MaterialTheme.typography.headlineMedium) + // Selected category persists across rotation (stored by name — null = the bare list on a phone). + var selectedName by rememberSaveable { mutableStateOf(null) } + val selected = selectedName?.let { n -> SettingsCategory.entries.firstOrNull { it.name == n } } - val (nw, nh, nhz) = nativeDisplayMode(context) + BoxWithConstraints(Modifier.fillMaxSize()) { + val twoPane = maxWidth >= 640.dp + // A two-column layout must never show an empty detail — land on the first category. + LaunchedEffect(twoPane) { + if (twoPane && selected == null) selectedName = SettingsCategory.Display.name + } - SettingsGroup("Display") { - SettingDropdown( - label = "Resolution", - options = RESOLUTION_OPTIONS.map { (w, h, lbl) -> - (w to h) to (if (w == 0) "$lbl ($nw × $nh)" else lbl) - }, - selected = s.width to s.height, - ) { (w, h) -> update(s.copy(width = w, height = h)) } - - SettingDropdown( - label = "Refresh rate", - options = REFRESH_OPTIONS.map { (hz, lbl) -> hz to (if (hz == 0) "$lbl ($nhz Hz)" else lbl) }, - selected = s.hz, - ) { hz -> update(s.copy(hz = hz)) } - - SettingDropdown( - label = "Bitrate", - options = BITRATE_OPTIONS, - selected = s.bitrateKbps, - ) { kbps -> update(s.copy(bitrateKbps = kbps)) } - - SettingDropdown( - label = "Video codec", - options = CODEC_OPTIONS, - selected = s.codec, - ) { c -> update(s.copy(codec = c)) } - - // HDR is only meaningful on a panel that can present HDR10; on an SDR display the toggle - // is disabled (and HDR is never advertised regardless) so the host doesn't send PQ the - // panel would mis-tone-map. The capability is fixed for the device, so read it once. - val hdrCapable = remember { displaySupportsHdr(context) } - ToggleRow( - title = "HDR", - subtitle = if (hdrCapable) { - "Stream 10-bit HDR (BT.2020 PQ) when the host supports it" - } else { - "This display can't present HDR10 — streams stay SDR" - }, - checked = s.hdrEnabled && hdrCapable, - enabled = hdrCapable, - onCheckedChange = { on -> update(s.copy(hdrEnabled = on)) }, + val detail: @Composable (SettingsCategory, (() -> Unit)?) -> Unit = { cat, back -> + CategoryDetail( + category = cat, + settings = s, + onChange = ::update, + context = context, + onMicChange = onMicChange, + onOpenControllers = { showControllers = true }, + onOpenLicenses = { showLicenses = true }, + onBack = back, ) } - SettingsGroup("Host") { - SettingDropdown( - label = "Compositor", - options = COMPOSITOR_OPTIONS.mapIndexed { i, lbl -> i to lbl }, - selected = s.compositor, - ) { c -> update(s.copy(compositor = c)) } - - SettingDropdown( - label = "Controller type", - options = GAMEPAD_OPTIONS.mapIndexed { i, lbl -> i to lbl }, - selected = s.gamepad, - ) { g -> update(s.copy(gamepad = g)) } - - ClickableRow( - title = "Connected controllers", - subtitle = "What the app detects, with a live input test", - onClick = { showControllers = true }, - ) - } - - SettingsGroup("Audio") { - SettingDropdown( - label = "Audio channels", - options = AUDIO_CHANNEL_OPTIONS, - selected = s.audioChannels, - ) { ch -> update(s.copy(audioChannels = ch)) } - - ToggleRow( - title = "Microphone", - subtitle = "Send your mic to the host's virtual microphone", - checked = s.micEnabled, - onCheckedChange = { on -> - when { - !on -> update(s.copy(micEnabled = false)) - ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO) == - PackageManager.PERMISSION_GRANTED -> update(s.copy(micEnabled = true)) - else -> micLauncher.launch(Manifest.permission.RECORD_AUDIO) + if (twoPane) { + BackHandler(onBack = onBack) + Row(Modifier.fillMaxSize()) { + CategoryList( + selected = selected, + twoPane = true, + onSelect = { selectedName = it.name }, + modifier = Modifier.width(300.dp).fillMaxHeight(), + ) + VerticalDivider() + Box(Modifier.weight(1f).fillMaxHeight()) { + // Cross-fade the detail pane as the selected category changes. + AnimatedContent( + targetState = selected ?: SettingsCategory.Display, + transitionSpec = { fadeIn(tween(200)) togetherWith fadeOut(tween(200)) }, + label = "SettingsPane", + ) { cat -> detail(cat, null) } + } + } + } else { + // Compact: the category list pushes to a full-screen detail and back, like the iOS / + // Android system settings — a horizontal slide that tracks the drill-in direction. + BackHandler { if (selected != null) selectedName = null else onBack() } + AnimatedContent( + targetState = selected, + transitionSpec = { + if (targetState != null) { + slideInHorizontally { it } + fadeIn() togetherWith + slideOutHorizontally { -it } + fadeOut() + } else { + slideInHorizontally { -it } + fadeIn() togetherWith + slideOutHorizontally { it } + fadeOut() } }, - ) - } - - SettingsGroup("Touch input") { - SettingDropdown( - label = "Touch input", - options = TOUCH_MODE_OPTIONS, - selected = s.touchMode, - onSelect = { mode -> update(s.copy(touchMode = mode)) }, - ) - Text( - "Trackpad: relative cursor like a laptop touchpad — tap to click, two-finger " + - "tap right-clicks, two fingers scroll, tap-then-drag holds the button. " + - "Direct pointer: the cursor jumps to your finger. Touch passthrough: real " + - "multi-touch reaches the host, for apps that understand touch.", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(top = 6.dp), - ) - } - - SettingsGroup("Overlay") { - ToggleRow( - title = "Stats overlay", - subtitle = "Show FPS, throughput and latency while streaming (3-finger tap toggles it live)", - checked = s.statsHudEnabled, - onCheckedChange = { on -> update(s.copy(statsHudEnabled = on)) }, - ) - } - - SettingsGroup("About") { - ClickableRow( - title = "Open-source licenses", - subtitle = "Third-party notices and credits", - onClick = { showLicenses = true }, - ) + label = "SettingsPush", + ) { sel -> + if (sel == null) { + CategoryList( + selected = null, + twoPane = false, + onSelect = { selectedName = it.name }, + modifier = Modifier.fillMaxSize(), + ) + } else { + detail(sel) { selectedName = null } + } + } } } } -/** A titled group of settings rendered inside an outlined card. */ +/** The top-level settings groups — each opens its own subpage (list on phone, split on tablet). */ +enum class SettingsCategory(val title: String, val icon: ImageVector) { + Display("Display", Icons.Filled.Tv), + Audio("Audio", Icons.Filled.VolumeUp), + Controls("Controls", Icons.Filled.SportsEsports), + Interface("Interface", Icons.Filled.Tune), + About("About", Icons.Filled.Info), +} + +/** The category list — the settings root. Highlights the [selected] row when it drives a detail pane. */ @Composable -private fun SettingsGroup(title: String, content: @Composable ColumnScope.() -> Unit) { - Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { +private fun CategoryList( + selected: SettingsCategory?, + twoPane: Boolean, + onSelect: (SettingsCategory) -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier + .verticalScroll(rememberScrollState()) + .padding(horizontal = 12.dp, vertical = 20.dp), + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { Text( - title, - style = MaterialTheme.typography.titleSmall, - color = MaterialTheme.colorScheme.primary, - modifier = Modifier.padding(start = 4.dp), + "Settings", + style = MaterialTheme.typography.headlineMedium, + modifier = Modifier.padding(start = 8.dp, bottom = 12.dp), ) - OutlinedCard(modifier = Modifier.fillMaxWidth()) { - Column( - modifier = Modifier.padding(16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp), - content = content, - ) + SettingsCategory.entries.forEach { cat -> + val highlighted = twoPane && selected == cat + Row( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(14.dp)) + .background(if (highlighted) MaterialTheme.colorScheme.secondaryContainer else Color.Transparent) + .clickable { onSelect(cat) } + .padding(horizontal = 14.dp, vertical = 15.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + cat.icon, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.padding(end = 16.dp), + ) + Text(cat.title, style = MaterialTheme.typography.bodyLarge, modifier = Modifier.weight(1f)) + if (!twoPane) { + Icon( + Icons.AutoMirrored.Filled.KeyboardArrowRight, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } } } } +/** One category's controls. [onBack] non-null (phone push) shows a back arrow; null (tablet pane) hides it. */ +@Composable +private fun CategoryDetail( + category: SettingsCategory, + settings: Settings, + onChange: (Settings) -> Unit, + context: android.content.Context, + onMicChange: (Boolean) -> Unit, + onOpenControllers: () -> Unit, + onOpenLicenses: () -> Unit, + onBack: (() -> Unit)?, +) { + Column( + Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(horizontal = 20.dp, vertical = 16.dp), + verticalArrangement = Arrangement.spacedBy(20.dp), + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + if (onBack != null) { + IconButton(onClick = onBack, modifier = Modifier.padding(end = 4.dp)) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back") + } + } + Text(category.title, style = MaterialTheme.typography.headlineMedium) + } + when (category) { + SettingsCategory.Display -> DisplaySettings(settings, onChange, context) + SettingsCategory.Audio -> AudioSettings(settings, onChange, onMicChange) + SettingsCategory.Controls -> ControlsSettings(settings, onChange, onOpenControllers) + SettingsCategory.Interface -> InterfaceSettings(settings, onChange) + SettingsCategory.About -> AboutSettings(onOpenLicenses) + } + } +} + +@Composable +private fun DisplaySettings(s: Settings, update: (Settings) -> Unit, context: android.content.Context) { + val (nw, nh, nhz) = nativeDisplayMode(context) + SettingsCard { + SettingDropdown( + label = "Resolution", + options = RESOLUTION_OPTIONS.map { (w, h, lbl) -> (w to h) to (if (w == 0) "$lbl ($nw × $nh)" else lbl) }, + selected = s.width to s.height, + ) { (w, h) -> update(s.copy(width = w, height = h)) } + + SettingDropdown( + label = "Refresh rate", + options = REFRESH_OPTIONS.map { (hz, lbl) -> hz to (if (hz == 0) "$lbl ($nhz Hz)" else lbl) }, + selected = s.hz, + ) { hz -> update(s.copy(hz = hz)) } + + SettingDropdown(label = "Bitrate", options = BITRATE_OPTIONS, selected = s.bitrateKbps) { kbps -> + update(s.copy(bitrateKbps = kbps)) + } + + SettingDropdown(label = "Video codec", options = CODEC_OPTIONS, selected = s.codec) { c -> + update(s.copy(codec = c)) + } + + // HDR is only meaningful on a panel that can present HDR10; on an SDR display the toggle is + // disabled (and HDR is never advertised) so the host doesn't send PQ the panel mis-tone-maps. + val hdrCapable = remember { displaySupportsHdr(context) } + ToggleRow( + title = "HDR", + subtitle = if (hdrCapable) { + "Stream 10-bit HDR (BT.2020 PQ) when the host supports it" + } else { + "This display can't present HDR10 — streams stay SDR" + }, + checked = s.hdrEnabled && hdrCapable, + enabled = hdrCapable, + onCheckedChange = { on -> update(s.copy(hdrEnabled = on)) }, + ) + + SettingDropdown( + label = "Compositor", + options = COMPOSITOR_OPTIONS.mapIndexed { i, lbl -> i to lbl }, + selected = s.compositor, + ) { c -> update(s.copy(compositor = c)) } + } +} + +@Composable +private fun AudioSettings(s: Settings, update: (Settings) -> Unit, onMicChange: (Boolean) -> Unit) { + SettingsCard { + SettingDropdown(label = "Audio channels", options = AUDIO_CHANNEL_OPTIONS, selected = s.audioChannels) { ch -> + update(s.copy(audioChannels = ch)) + } + ToggleRow( + title = "Microphone", + subtitle = "Send your mic to the host's virtual microphone", + checked = s.micEnabled, + onCheckedChange = onMicChange, + ) + } +} + +@Composable +private fun ControlsSettings(s: Settings, update: (Settings) -> Unit, onOpenControllers: () -> Unit) { + SettingsCard { + SettingDropdown(label = "Touch input", options = TOUCH_MODE_OPTIONS, selected = s.touchMode) { mode -> + update(s.copy(touchMode = mode)) + } + Text( + "Trackpad: relative cursor like a laptop touchpad — tap to click, two-finger tap " + + "right-clicks, two fingers scroll, tap-then-drag holds the button. Direct pointer: " + + "the cursor jumps to your finger. Touch passthrough: real multi-touch reaches the " + + "host, for apps that understand touch.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + SettingsCard { + SettingDropdown( + label = "Controller type", + options = GAMEPAD_OPTIONS.mapIndexed { i, lbl -> i to lbl }, + selected = s.gamepad, + ) { g -> update(s.copy(gamepad = g)) } + ClickableRow( + title = "Connected controllers", + subtitle = "What the app detects, with a live input test", + onClick = onOpenControllers, + ) + } +} + +@Composable +private fun InterfaceSettings(s: Settings, update: (Settings) -> Unit) { + SettingsCard { + ToggleRow( + title = "Controller-optimized UI", + subtitle = "Switch to the console home (host carousel) when a controller is connected", + checked = s.gamepadUiEnabled, + onCheckedChange = { on -> update(s.copy(gamepadUiEnabled = on)) }, + ) + ToggleRow( + title = "Game library", + subtitle = "Browse a paired host's game library (press Y on a saved host)", + checked = s.libraryEnabled, + onCheckedChange = { on -> update(s.copy(libraryEnabled = on)) }, + ) + ToggleRow( + title = "Stats overlay", + subtitle = "Show FPS, throughput and latency while streaming (3-finger tap toggles it live)", + checked = s.statsHudEnabled, + onCheckedChange = { on -> update(s.copy(statsHudEnabled = on)) }, + ) + } +} + +@Composable +private fun AboutSettings(onOpenLicenses: () -> Unit) { + SettingsCard { + ClickableRow( + title = "Open-source licenses", + subtitle = "Third-party notices and credits", + onClick = onOpenLicenses, + ) + } +} + +/** A group of settings rendered inside an outlined card. */ +@Composable +private fun SettingsCard(content: @Composable ColumnScope.() -> Unit) { + OutlinedCard(modifier = Modifier.fillMaxWidth()) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + content = content, + ) + } +} + /** A title + subtitle on the left, a Switch on the right. [enabled] greys out the whole row. */ @Composable private fun ToggleRow( @@ -265,6 +461,12 @@ private fun ClickableRow(title: String, subtitle: String, onClick: () -> Unit) { color = MaterialTheme.colorScheme.onSurfaceVariant, ) } + Icon( + Icons.AutoMirrored.Filled.KeyboardArrowRight, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(20.dp), + ) } } diff --git a/clients/android/app/src/main/kotlin/io/unom/punktfunk/StreamScreen.kt b/clients/android/app/src/main/kotlin/io/unom/punktfunk/StreamScreen.kt index ab14f25..9ce71d6 100644 --- a/clients/android/app/src/main/kotlin/io/unom/punktfunk/StreamScreen.kt +++ b/clients/android/app/src/main/kotlin/io/unom/punktfunk/StreamScreen.kt @@ -91,6 +91,8 @@ fun StreamScreen(handle: Long, micEnabled: Boolean, onDisconnect: () -> Unit) { activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE activity?.streamHandle = handle // route hardware keys to this session activity?.axisMapper = Gamepad.AxisMapper(handle) // route joystick axes + activity?.requestStreamExit = onDisconnect // Select+Start+L1+R1 chord leaves the stream + activity?.setConsoleHighRefreshRate(false) // let the decoder's setFrameRate pick the panel rate // Host→client feedback (rumble + DualSense lightbar/LEDs); poll threads stopped before close. val feedback = GamepadFeedback(handle).also { it.start() } onDispose { @@ -99,6 +101,8 @@ fun StreamScreen(handle: Long, micEnabled: Boolean, onDisconnect: () -> Unit) { activity?.axisMapper?.reset() // release-all so nothing sticks on the host activity?.axisMapper = null activity?.streamHandle = 0L + activity?.requestStreamExit = null + activity?.setConsoleHighRefreshRate(true) // back to the console UI's max refresh controller?.show(WindowInsetsCompat.Type.systemBars()) window?.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) // Release the landscape lock so the rest of the app follows the device/system again. diff --git a/clients/android/app/src/main/kotlin/io/unom/punktfunk/Theme.kt b/clients/android/app/src/main/kotlin/io/unom/punktfunk/Theme.kt index 9e2687e..8858ef3 100644 --- a/clients/android/app/src/main/kotlin/io/unom/punktfunk/Theme.kt +++ b/clients/android/app/src/main/kotlin/io/unom/punktfunk/Theme.kt @@ -41,5 +41,7 @@ fun PunktfunkTheme(content: @Composable () -> Unit) { } else { BrandDark } - MaterialTheme(colorScheme = scheme, content = content) + // Geist Sans across the whole type scale — the brand typeface the website and the Apple client + // already ship (see Type.kt). + MaterialTheme(colorScheme = scheme, typography = PunktfunkTypography, content = content) } diff --git a/clients/android/app/src/main/kotlin/io/unom/punktfunk/Type.kt b/clients/android/app/src/main/kotlin/io/unom/punktfunk/Type.kt new file mode 100644 index 0000000..733ce01 --- /dev/null +++ b/clients/android/app/src/main/kotlin/io/unom/punktfunk/Type.kt @@ -0,0 +1,44 @@ +package io.unom.punktfunk + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight + +// Geist — the punktfunk brand typeface (the same family the website and the Apple client ship). +// Bundled as static OTF weights in res/font and applied to every Material 3 text style below, so the +// Android UI carries the brand type identically to the other clients. Geist Sans only — Geist Mono +// is intentionally not shipped (the licenses screen's technical block uses the platform monospace). +// +// Licensed under the SIL Open Font License 1.1 (see the Geist OFL entry in THIRD-PARTY-NOTICES.txt). +val Geist = FontFamily( + Font(R.font.geist_regular, FontWeight.Normal), + Font(R.font.geist_medium, FontWeight.Medium), + Font(R.font.geist_semibold, FontWeight.SemiBold), + Font(R.font.geist_bold, FontWeight.Bold), +) + +/** + * The default Material 3 type scale re-based on [Geist]. Material 3's [Typography] has no + * `defaultFontFamily` shortcut (that was Material 2), so each of the 15 roles is re-emitted with the + * Geist family while keeping Material's sizes, line heights, letter spacing and per-role weights. + */ +val PunktfunkTypography: Typography = Typography().run { + Typography( + displayLarge = displayLarge.copy(fontFamily = Geist), + displayMedium = displayMedium.copy(fontFamily = Geist), + displaySmall = displaySmall.copy(fontFamily = Geist), + headlineLarge = headlineLarge.copy(fontFamily = Geist), + headlineMedium = headlineMedium.copy(fontFamily = Geist), + headlineSmall = headlineSmall.copy(fontFamily = Geist), + titleLarge = titleLarge.copy(fontFamily = Geist), + titleMedium = titleMedium.copy(fontFamily = Geist), + titleSmall = titleSmall.copy(fontFamily = Geist), + bodyLarge = bodyLarge.copy(fontFamily = Geist), + bodyMedium = bodyMedium.copy(fontFamily = Geist), + bodySmall = bodySmall.copy(fontFamily = Geist), + labelLarge = labelLarge.copy(fontFamily = Geist), + labelMedium = labelMedium.copy(fontFamily = Geist), + labelSmall = labelSmall.copy(fontFamily = Geist), + ) +} diff --git a/clients/android/app/src/main/kotlin/io/unom/punktfunk/WakeController.kt b/clients/android/app/src/main/kotlin/io/unom/punktfunk/WakeController.kt new file mode 100644 index 0000000..070d62a --- /dev/null +++ b/clients/android/app/src/main/kotlin/io/unom/punktfunk/WakeController.kt @@ -0,0 +1,125 @@ +package io.unom.punktfunk + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import io.unom.punktfunk.kit.NativeBridge +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch + +/** + * Wake a sleeping host and WAIT for it to come back before proceeding — the Android mirror of the + * Apple client's `HostWaker`. + * + * A magic packet is fire-and-forget, and a cold box can take 20–60 s to POST, boot, and start + * advertising on mDNS again — far longer than a connect attempt will sit. So instead of firing one + * packet and immediately dialing (which just fails on a genuinely-asleep host), this drives a visible + * "Waking…" state: it (re-)sends the packet, polls the host's mDNS presence once a second via + * [isOnline], and on success runs [onOnline] (the real connect for a Wake-&-Connect, or nothing for + * a wake-only); on timeout it parks in a retry/cancel state. One wake at a time. + * + * [scope] is the composition's coroutine scope (main-dispatched), so [waking] mutations and the + * [isOnline]/[onOnline] callbacks all run on the main thread; only the blocking send is off-loaded. + */ +class WakeController(private val scope: CoroutineScope) { + /** null = idle; non-null drives [WakeOverlay]. */ + data class Waking( + val hostName: String, + /** Whether coming online chains into a connect (Wake & Connect) vs. just stopping. */ + val connectsAfter: Boolean, + val seconds: Int = 0, + val timedOut: Boolean = false, + ) + + var waking by mutableStateOf(null) + private set + + private var loop: Job? = null + + /** Captured so "Try Again" replays the exact same wait. */ + private var replay: (() -> Unit)? = null + + /** + * Wake the host and wait for [isOnline] to go true, then run [onOnline]. [macs]/[lastIp] target + * the magic packet. No-ops straight to [onOnline] when there's nothing to wake with or the host + * is already up (a race between the caller's check and here). + */ + fun start( + hostName: String, + connectsAfter: Boolean, + macs: List, + lastIp: String, + isOnline: () -> Boolean, + onOnline: () -> Unit, + ) { + if (macs.isEmpty() || isOnline()) { + cancel() + onOnline() + return + } + replay = { run(hostName, connectsAfter, macs, lastIp, isOnline, onOnline) } + replay?.invoke() + } + + /** Stop waiting and dismiss the overlay (B / Cancel). */ + fun cancel() { + loop?.cancel() + loop = null + replay = null + waking = null + } + + /** Restart the wait after a timeout (A / Try Again). */ + fun retry() { + replay?.invoke() + } + + private fun run( + hostName: String, + connectsAfter: Boolean, + macs: List, + lastIp: String, + isOnline: () -> Boolean, + onOnline: () -> Unit, + ) { + loop?.cancel() + waking = Waking(hostName = hostName, connectsAfter = connectsAfter) + loop = scope.launch { + var elapsed = 0 + while (isActive) { + // Re-send periodically: a single packet can be missed, and some NICs only wake on a + // fresh packet after dropping into a deeper sleep state. + if (elapsed % RESEND_EVERY_S == 0) { + val csv = macs.joinToString(",") + launch(Dispatchers.IO) { NativeBridge.nativeWakeOnLan(csv, lastIp) } + } + if (isOnline()) { + waking = null + loop = null + onOnline() + return@launch + } + if (elapsed >= TIMEOUT_S) { + waking = waking?.copy(timedOut = true) + loop = null + return@launch + } + delay(1000) + elapsed++ + waking = waking?.copy(seconds = elapsed) + } + } + } + + companion object { + /** How long to wait for the host to reappear before giving up (a cold boot can be a minute+). */ + const val TIMEOUT_S = 90 + + /** Re-send the magic packet this often. */ + const val RESEND_EVERY_S = 6 + } +} diff --git a/clients/android/app/src/main/kotlin/io/unom/punktfunk/WakeOverlay.kt b/clients/android/app/src/main/kotlin/io/unom/punktfunk/WakeOverlay.kt new file mode 100644 index 0000000..e91292e --- /dev/null +++ b/clients/android/app/src/main/kotlin/io/unom/punktfunk/WakeOverlay.kt @@ -0,0 +1,124 @@ +package io.unom.punktfunk + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Bedtime +import androidx.compose.material3.Button +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +/** + * The "Waking …" modal shown while [WakeController] brings a sleeping host back — a spinner + a + * live elapsed counter, escalating to a retry/cancel prompt on timeout. The Android mirror of the + * Apple client's `WakeOverlay`. Rendered over BOTH the touch grid and the console home; it swallows + * input to the screen behind it, and in console mode the pad drives it (B cancels, A retries once + * timed out) while the touch buttons work for a pointer. + */ +@Composable +fun WakeOverlay(waker: WakeController, gamepadUi: Boolean) { + val w = waker.waking ?: return + + BackHandler { waker.cancel() } // system Back / pad B (remapped) cancels the wait + if (gamepadUi) { + // A retries once timed out; B falls through to the BackHandler above. + GamepadNavEffect2D( + active = true, + onDirection = {}, + onActivate = { if (w.timedOut) waker.retry() }, + ) + } + + Box( + Modifier + .fillMaxSize() + .background(Color.Black.copy(alpha = 0.6f)) + // Swallow taps so the home behind can't be touched while waking. + .clickable(interactionSource = remember { MutableInteractionSource() }, indication = null) {}, + contentAlignment = Alignment.Center, + ) { + Column( + Modifier + .padding(40.dp) + .widthIn(max = 380.dp) + .clip(RoundedCornerShape(22.dp)) + .background(Color(0xF01A1730)) + .border(1.dp, Color.White.copy(alpha = 0.12f), RoundedCornerShape(22.dp)) + .padding(28.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(14.dp), + ) { + if (w.timedOut) { + Icon( + Icons.Filled.Bedtime, + contentDescription = null, + tint = Color.White.copy(alpha = 0.85f), + modifier = Modifier.size(34.dp), + ) + Text( + "${w.hostName} didn't wake", + color = Color.White, + fontWeight = FontWeight.Bold, + fontSize = 19.sp, + textAlign = TextAlign.Center, + ) + Text( + "It may still be booting, or it's powered off / off this network.", + color = Color.White.copy(alpha = 0.6f), + fontSize = 13.sp, + textAlign = TextAlign.Center, + ) + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier.padding(top = 6.dp), + ) { + OutlinedButton(onClick = { waker.cancel() }) { Text("Cancel") } + Button(onClick = { waker.retry() }) { Text("Try Again") } + } + } else { + CircularProgressIndicator(color = Color.White) + Text( + "Waking ${w.hostName}…", + color = Color.White, + fontWeight = FontWeight.Bold, + fontSize = 19.sp, + textAlign = TextAlign.Center, + ) + Text( + "Waiting for it to come online · ${w.seconds}s", + color = Color.White.copy(alpha = 0.6f), + fontSize = 13.sp, + fontFamily = FontFamily.Monospace, + ) + OutlinedButton(onClick = { waker.cancel() }, modifier = Modifier.padding(top = 6.dp)) { + Text(if (w.connectsAfter) "Cancel" else "Stop Waiting") + } + } + } + } +} diff --git a/clients/android/app/src/main/kotlin/io/unom/punktfunk/components/HostComponents.kt b/clients/android/app/src/main/kotlin/io/unom/punktfunk/components/HostComponents.kt index 43b34e9..1e83087 100644 --- a/clients/android/app/src/main/kotlin/io/unom/punktfunk/components/HostComponents.kt +++ b/clients/android/app/src/main/kotlin/io/unom/punktfunk/components/HostComponents.kt @@ -59,7 +59,7 @@ fun HostCard( enabled: Boolean, onConnect: () -> Unit, onForget: (() -> Unit)?, - onRename: (() -> Unit)? = null, + onEdit: (() -> Unit)? = null, onWake: (() -> Unit)? = null, ) { // D-pad / controller focus highlight: a clickable card is focusable, but the default state @@ -108,7 +108,7 @@ fun HostCard( StatusPill(status) } - if (onForget != null || onRename != null || onWake != null) { + if (onForget != null || onEdit != null || onWake != null) { var menu by remember { mutableStateOf(false) } Box(modifier = Modifier.align(Alignment.TopEnd)) { IconButton(enabled = enabled, onClick = { menu = true }) { @@ -129,12 +129,12 @@ fun HostCard( }, ) } - if (onRename != null) { + if (onEdit != null) { DropdownMenuItem( - text = { Text("Rename") }, + text = { Text("Edit…") }, onClick = { menu = false - onRename() + onEdit() }, ) } diff --git a/clients/android/app/src/main/res/drawable-hdpi/tv_banner.png b/clients/android/app/src/main/res/drawable-hdpi/tv_banner.png new file mode 100644 index 0000000000000000000000000000000000000000..85ac4b28b2b813c4edbafdf450c1e27ffd30e920 GIT binary patch literal 5109 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 NO7iO9YFUer{{h(K_{0DJ literal 0 HcmV?d00001 diff --git a/clients/android/app/src/main/res/drawable-xxhdpi/tv_banner.png b/clients/android/app/src/main/res/drawable-xxhdpi/tv_banner.png new file mode 100644 index 0000000000000000000000000000000000000000..77e1b25f1a8e49c7d9e425ecb4d859a81fe78eec GIT binary patch literal 9831 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 literal 0 HcmV?d00001 diff --git a/clients/android/app/src/main/res/font/geist_bold.otf b/clients/android/app/src/main/res/font/geist_bold.otf new file mode 100644 index 0000000000000000000000000000000000000000..6ab5615f06c0e26cc8dcea4300f6e5af1a3e5955 GIT binary patch literal 166516 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 literal 0 HcmV?d00001 diff --git a/clients/android/app/src/main/res/font/geist_medium.otf b/clients/android/app/src/main/res/font/geist_medium.otf new file mode 100644 index 0000000000000000000000000000000000000000..99fb7c28266040b6097966ac3b371a7091259b66 GIT binary patch literal 162304 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®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 literal 0 HcmV?d00001 diff --git a/clients/android/app/src/main/res/font/geist_regular.otf b/clients/android/app/src/main/res/font/geist_regular.otf new file mode 100644 index 0000000000000000000000000000000000000000..82878335eb0faf730aa9932f5078f90869ae035c GIT binary patch literal 157508 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 UH+BJaczmQP)ECDt@m^H_1ycU@xc~qF literal 0 HcmV?d00001 diff --git a/clients/android/app/src/main/res/font/geist_semibold.otf b/clients/android/app/src/main/res/font/geist_semibold.otf new file mode 100644 index 0000000000000000000000000000000000000000..277a521af67aa019c9191e902191eecd0929f81f GIT binary patch literal 164780 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 AI=QtTE8j_GrgTC~^mKqi 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 literal 0 HcmV?d00001 diff --git a/clients/android/app/src/test/kotlin/io/unom/punktfunk/screenshots/ShotScenes.kt b/clients/android/app/src/test/kotlin/io/unom/punktfunk/screenshots/ShotScenes.kt index 7cd3893..e2a5a6e 100644 --- a/clients/android/app/src/test/kotlin/io/unom/punktfunk/screenshots/ShotScenes.kt +++ b/clients/android/app/src/test/kotlin/io/unom/punktfunk/screenshots/ShotScenes.kt @@ -83,7 +83,7 @@ internal fun HostsScene() { } item(span = { GridItemSpan(maxLineSpan) }) { SectionLabel("Saved hosts") } items(SAVED) { h -> - HostCard(h.name, h.address, h.status, enabled = true, onConnect = {}, onForget = {}, onRename = {}) + HostCard(h.name, h.address, h.status, enabled = true, onConnect = {}, onForget = {}, onEdit = {}) } item(span = { GridItemSpan(maxLineSpan) }) { Spacer(Modifier.height(12.dp)) diff --git a/clients/android/kit/build.gradle.kts b/clients/android/kit/build.gradle.kts index 6f8e82c..61d0618 100644 --- a/clients/android/kit/build.gradle.kts +++ b/clients/android/kit/build.gradle.kts @@ -15,8 +15,10 @@ android { ndkVersion = ndkVer defaultConfig { - minSdk = 31 - ndk { abiFilters += listOf("arm64-v8a", "x86_64") } + minSdk = 28 // Android 9 — reaches older TV boxes; API 31+ features are runtime-gated. + // Keep in lockstep with :app — 32-bit armeabi-v7a for the many 32-bit Google TV / Android TV + // boxes, 64-bit arm64-v8a for phones + modern TV, x86_64 for the emulator. + ndk { abiFilters += listOf("arm64-v8a", "armeabi-v7a", "x86_64") } } compileOptions { sourceCompatibility = JavaVersion.VERSION_21 @@ -28,6 +30,9 @@ android { kotlin { compilerOptions { jvmTarget.set(JvmTarget.JVM_21) } } dependencies { + // mTLS HTTPS client for the host's management API (the game-library fetch + cover-art loads). + // OkHttp lets us present the paired client cert and pin the host's self-signed cert by SHA-256. + implementation("com.squareup.okhttp3:okhttp:4.12.0") testImplementation("junit:junit:4.13.2") // JVM unit test for the pure TXT parser } @@ -85,9 +90,11 @@ fun registerCargoNdk(taskName: String, release: Boolean) = // find their subtools. val cmd = mutableListOf( "$cargoBin/cargo", "ndk", - "-t", "arm64-v8a", "-t", "x86_64", - // Link against the minSdk-31 sysroot so libaaudio (API 26+) is found. - "--platform", "31", + "-t", "arm64-v8a", "-t", "armeabi-v7a", "-t", "x86_64", + // Link against the minSdk-28 sysroot: libaaudio (API 26) is present, and building at the + // floor makes the linker reject any accidental >28 hard import (the one API-30 call we + // make, ANativeWindow_setFrameRate, is dlsym-resolved — see decode::try_set_frame_rate). + "--platform", "28", "-o", file("src/main/jniLibs").absolutePath, "build", "-p", "punktfunk-client-android", ) diff --git a/clients/android/kit/src/main/kotlin/io/unom/punktfunk/kit/GamepadFeedback.kt b/clients/android/kit/src/main/kotlin/io/unom/punktfunk/kit/GamepadFeedback.kt index bc42af8..2c37cf7 100644 --- a/clients/android/kit/src/main/kotlin/io/unom/punktfunk/kit/GamepadFeedback.kt +++ b/clients/android/kit/src/main/kotlin/io/unom/punktfunk/kit/GamepadFeedback.kt @@ -8,6 +8,7 @@ import android.hardware.lights.LightsRequest import android.os.Build import android.os.CombinedVibration import android.os.VibrationEffect +import android.os.Vibrator import android.os.VibratorManager import android.util.Log import android.view.InputDevice @@ -16,7 +17,8 @@ import java.nio.ByteBuffer /** * Host→client gamepad feedback for one session (single-pad model — pad 0 only). Two daemon poll * threads drain the blocking native pulls and render in Kotlin: rumble → the controller's - * `VibratorManager`; HID-output → lightbar / player-LED via `LightsManager` (API 33+); adaptive + * `VibratorManager` (API 31+) or its single legacy `Vibrator` on API 28–30; HID-output → lightbar / + * player-LED via `LightsManager` (API 33+); adaptive * triggers are parse-validated and logged (Android has no public adaptive-trigger API). * * Mirrors `nativeStartAudio`'s lifecycle: [start]/[stop] driven by the StreamScreen. [stop] flips a @@ -40,6 +42,9 @@ class GamepadFeedback(private val handle: Long) { private var hidoutThread: Thread? = null private var vm: VibratorManager? = null + // API 28–30 fallback: the controller's single legacy Vibrator (no per-motor VibratorManager + // until API 31). Exactly one of [vm] / [legacy] is bound; rumble degrades to one blended motor. + private var legacy: Vibrator? = null private var vibratorIds: IntArray = IntArray(0) private var amplitudeControlled = false @@ -81,6 +86,7 @@ class GamepadFeedback(private val handle: Long) { rumbleThread?.interrupt() hidoutThread?.interrupt() runCatching { vm?.cancel() } // drop any held rumble immediately + runCatching { legacy?.cancel() } // Join WITHOUT a timeout. These poll threads dereference the native session handle on every // pull (nativeNextRumble/nativeNextHidout), so they MUST be dead before StreamScreen's // onDispose reaches nativeClose, which frees that handle. A *bounded* join that times out @@ -98,6 +104,7 @@ class GamepadFeedback(private val handle: Long) { rgbLight = null playerLight = null vm = null + legacy = null vibratorIds = IntArray(0) } @@ -111,39 +118,65 @@ class GamepadFeedback(private val handle: Long) { Log.i(TAG, "rumble: no controller connected — rumble no-op (emulator path)") return } - val m = dev.vibratorManager - val ids = m.vibratorIds - if (ids.isEmpty()) { - Log.i(TAG, "rumble: controller '${dev.name}' has no vibrators — rumble no-op") - return + if (Build.VERSION.SDK_INT >= 31) { + val m = dev.vibratorManager + val ids = m.vibratorIds + if (ids.isEmpty()) { + Log.i(TAG, "rumble: controller '${dev.name}' has no vibrators — rumble no-op") + return + } + vm = m + vibratorIds = ids + amplitudeControlled = ids.all { m.getVibrator(it).hasAmplitudeControl() } + Log.i(TAG, "rumble: bound ${ids.size} vibrators amplitudeControl=$amplitudeControlled") + } else { + // API 28–30: no VibratorManager — fall back to the controller's single legacy Vibrator. + @Suppress("DEPRECATION") + val v = dev.vibrator + if (!v.hasVibrator()) { + Log.i(TAG, "rumble: controller '${dev.name}' has no vibrator — rumble no-op") + return + } + legacy = v + amplitudeControlled = v.hasAmplitudeControl() + Log.i(TAG, "rumble: bound legacy vibrator amplitudeControl=$amplitudeControlled") } - vm = m - vibratorIds = ids - amplitudeControlled = ids.all { m.getVibrator(it).hasAmplitudeControl() } - Log.i(TAG, "rumble: bound ${ids.size} vibrators amplitudeControl=$amplitudeControlled") } /** low = heavy/left motor, high = light/right motor; both 0..0xFFFF (the host's u16 amplitudes). */ private fun renderRumble(low: Int, high: Int) { Log.i(TAG, "rumble low=$low high=$high") // verification line — BEFORE any no-op return - val m = vm ?: return val lo = toAmplitude(low) val hi = toAmplitude(high) - if (lo == 0 && hi == 0) { - m.cancel() // (0,0) = stop + val m = vm + if (m != null) { + if (lo == 0 && hi == 0) { + m.cancel() // (0,0) = stop + return + } + val combo = CombinedVibration.startParallel() + if (amplitudeControlled && vibratorIds.size >= 2) { + // ids[0] = light/right, ids[1] = heavy/left (XInput/Moonlight convention). + if (hi != 0) combo.addVibrator(vibratorIds[0], oneShot(hi)) + if (lo != 0) combo.addVibrator(vibratorIds[1], oneShot(lo)) + } else { + // Single motor or no amplitude control: blend both into one effect. + val a = (lo * 0.8 + hi * 0.33).toInt().coerceIn(1, 255) + for (id in vibratorIds) combo.addVibrator(id, oneShot(a)) + } + runCatching { m.vibrate(combo.combine()) } return } - val combo = CombinedVibration.startParallel() - if (amplitudeControlled && vibratorIds.size >= 2) { - // ids[0] = light/right, ids[1] = heavy/left (XInput/Moonlight convention). - if (hi != 0) combo.addVibrator(vibratorIds[0], oneShot(hi)) - if (lo != 0) combo.addVibrator(vibratorIds[1], oneShot(lo)) - } else { - // Single motor or no amplitude control: blend both into one effect. - val a = (lo * 0.8 + hi * 0.33).toInt().coerceIn(1, 255) - for (id in vibratorIds) combo.addVibrator(id, oneShot(a)) + // API 28–30 legacy single-motor path: blend both motors into one effect. + val lv = legacy ?: return + if (lo == 0 && hi == 0) { + lv.cancel() // (0,0) = stop + return + } + val a = (lo * 0.8 + hi * 0.33).toInt().coerceIn(1, 255) + runCatching { + lv.vibrate(if (amplitudeControlled) oneShot(a) else oneShot(VibrationEffect.DEFAULT_AMPLITUDE)) } - runCatching { m.vibrate(combo.combine()) } } // 0..0xFFFF → 1..255 (high byte); a nonzero motor never collapses to 0. diff --git a/clients/android/kit/src/main/kotlin/io/unom/punktfunk/kit/library/Library.kt b/clients/android/kit/src/main/kotlin/io/unom/punktfunk/kit/library/Library.kt new file mode 100644 index 0000000..12b4d1e --- /dev/null +++ b/clients/android/kit/src/main/kotlin/io/unom/punktfunk/kit/library/Library.kt @@ -0,0 +1,195 @@ +package io.unom.punktfunk.kit.library + +import okhttp3.OkHttpClient +import okhttp3.Request +import org.json.JSONArray +import org.json.JSONObject +import java.io.ByteArrayInputStream +import java.security.KeyFactory +import java.security.KeyStore +import java.security.MessageDigest +import java.security.PrivateKey +import java.security.cert.CertificateFactory +import java.security.cert.X509Certificate +import java.security.spec.PKCS8EncodedKeySpec +import java.util.Base64 +import java.util.concurrent.TimeUnit +import javax.net.ssl.HostnameVerifier +import javax.net.ssl.HttpsURLConnection +import javax.net.ssl.KeyManagerFactory +import javax.net.ssl.SSLContext +import javax.net.ssl.TrustManager +import javax.net.ssl.TrustManagerFactory +import javax.net.ssl.X509TrustManager + +// Android game-library client — the mirror of the Apple client's LibraryClient.swift. Fetches a +// host's unified game library from its management REST API (`GET /api/v1/library`) over **mTLS**: the +// paired client presents its persistent cert/key (the same identity the host paired over QUIC), and +// the host's self-signed cert is pinned by SHA-256(DER). Read-only. Mirrors the GameEntry/Artwork +// schema in crates/punktfunk-host/src/library.rs. + +/** The management API's default port — matches `mgmt::DEFAULT_PORT` on the host and the Apple client. */ +const val DEFAULT_MGMT_PORT = 47990 + +/** Cover-art URLs. Steam art arrives as host-relative proxy paths, resolved to absolute by [LibraryClient]. */ +data class Artwork(val portrait: String?, val header: String?, val hero: String?) { + /** Poster preference for a 2:3 tile: portrait capsule → header → hero (near-universal fallbacks). */ + val posterCandidates: List get() = listOfNotNull(portrait, header, hero) +} + +/** One title in the unified library. [id] is store-qualified (`steam:` / `custom:`). */ +data class GameEntry(val id: String, val store: String, val title: String, val art: Artwork) { + val isCustom: Boolean get() = store == "custom" +} + +/** Fetch outcome — three states so the UI can guide setup (the common case is "not paired yet"). */ +sealed class LibraryResult { + data class Ok(val games: List) : LibraryResult() + data class Unauthorized(val message: String) : LibraryResult() + data class Error(val message: String) : LibraryResult() +} + +object LibraryClient { + /** + * `GET https://

:/api/v1/library`, authenticated by mTLS. [fpHex] is the pinned + * host-cert SHA-256 (64 hex, from the paired [io.unom.punktfunk.kit.security.KnownHost]); a blank + * value means the host was never connected/paired, so there's nothing authorized to browse. + * BLOCKING — call from a background dispatcher. + */ + fun fetch( + address: String, + mgmtPort: Int = DEFAULT_MGMT_PORT, + certPem: String, + keyPem: String, + fpHex: String, + ): LibraryResult { + if (fpHex.isBlank()) { + return LibraryResult.Unauthorized( + "Connect to this host once first — the library uses the identity created on pairing to authenticate.", + ) + } + val client = try { + mtlsHttpClient(certPem, keyPem, address, fpHex) + } catch (e: Exception) { + return LibraryResult.Error("Couldn't set up the secure connection: ${e.message}") + } + val base = "https://$address:$mgmtPort" + val req = Request.Builder().url("$base/api/v1/library").build() + return try { + client.newCall(req).execute().use { resp -> + when (resp.code) { + 200 -> LibraryResult.Ok(parse(resp.body?.string().orEmpty(), base)) + 401 -> LibraryResult.Unauthorized( + "The host didn't recognize this device. Pair with the host first — it authorizes paired clients by their certificate.", + ) + else -> LibraryResult.Error("The management API returned HTTP ${resp.code}.") + } + } + } catch (e: Exception) { + LibraryResult.Error( + "Couldn't reach the host's management API: ${e.message}. It binds the LAN by default, so check the host is updated and reachable.", + ) + } + } + + private fun parse(json: String, base: String): List { + val arr = JSONArray(json) + val out = ArrayList(arr.length()) + for (i in 0 until arr.length()) { + val o = arr.getJSONObject(i) + val art = o.optJSONObject("art") ?: JSONObject() + out.add( + GameEntry( + id = o.optString("id"), + store = o.optString("store"), + title = o.optString("title"), + art = Artwork( + portrait = resolveArt(str(art, "portrait"), base), + header = resolveArt(str(art, "header"), base), + hero = resolveArt(str(art, "hero"), base), + ), + ), + ) + } + return out + } + + /** A present, non-null, non-blank JSON string field, else null. */ + private fun str(o: JSONObject, key: String): String? = + if (o.has(key) && !o.isNull(key)) o.optString(key).ifBlank { null } else null + + /** Host-relative art path (`/api/v1/library/art/...`) → absolute against the host; else unchanged. */ + private fun resolveArt(s: String?, base: String): String? = + if (s != null && s.startsWith("/")) base + s else s +} + +/** + * An OkHttpClient that presents the paired client cert and pins the host's self-signed cert by + * SHA-256(DER) — reused for BOTH the library fetch and the cover-art loads (so a paired client + * reaches the host's own art proxy). The pinning trust manager trusts the host by fingerprint and + * defers to normal public trust for any other origin (an external CDN URL); the hostname verifier + * accepts the pinned host (whose self-signed cert has no matching SAN) and defers otherwise. + */ +fun mtlsHttpClient(certPem: String, keyPem: String, host: String, fpHex: String): OkHttpClient { + val clientCert = CertificateFactory.getInstance("X.509") + .generateCertificate(ByteArrayInputStream(certPem.toByteArray())) as X509Certificate + val privateKey = parsePrivateKey(keyPem) + + val keyStore = KeyStore.getInstance("PKCS12").apply { + load(null, null) + setKeyEntry("client", privateKey, CharArray(0), arrayOf(clientCert)) + } + val kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()) + kmf.init(keyStore, CharArray(0)) + + // System default trust manager, for non-host (external CDN) origins. + val sysTmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) + sysTmf.init(null as KeyStore?) + val sysTm = sysTmf.trustManagers.filterIsInstance().first() + + val pinned = fpHex.lowercase() + val trustManager = object : X509TrustManager { + override fun checkClientTrusted(chain: Array, authType: String) {} + override fun checkServerTrusted(chain: Array, authType: String) { + if (sha256Hex(chain[0].encoded) == pinned) return // the pinned host + sysTm.checkServerTrusted(chain, authType) // external CDN — normal public trust + } + override fun getAcceptedIssuers(): Array = sysTm.acceptedIssuers + } + + val ssl = SSLContext.getInstance("TLS") + ssl.init(kmf.keyManagers, arrayOf(trustManager), null) + + val defaultVerifier = HttpsURLConnection.getDefaultHostnameVerifier() + val verifier = HostnameVerifier { hostname, session -> + hostname == host || defaultVerifier.verify(hostname, session) + } + + return OkHttpClient.Builder() + .sslSocketFactory(ssl.socketFactory, trustManager) + .hostnameVerifier(verifier) + .connectTimeout(8, TimeUnit.SECONDS) + .readTimeout(15, TimeUnit.SECONDS) + .build() +} + +/** Parse a PKCS#8 PEM private key (rcgen emits `-----BEGIN PRIVATE KEY-----`), trying EC then RSA/Ed25519. */ +private fun parsePrivateKey(pem: String): PrivateKey { + val body = pem + .replace(Regex("-----BEGIN [A-Z ]*PRIVATE KEY-----"), "") + .replace(Regex("-----END [A-Z ]*PRIVATE KEY-----"), "") + .replace(Regex("\\s"), "") + val der = Base64.getDecoder().decode(body) + val spec = PKCS8EncodedKeySpec(der) + for (alg in listOf("EC", "RSA", "Ed25519")) { + try { + return KeyFactory.getInstance(alg).generatePrivate(spec) + } catch (_: Exception) { + // try the next algorithm + } + } + throw IllegalArgumentException("unsupported private-key format (not EC/RSA/Ed25519 PKCS#8)") +} + +private fun sha256Hex(der: ByteArray): String = + MessageDigest.getInstance("SHA-256").digest(der).joinToString("") { "%02x".format(it) } diff --git a/clients/android/kit/src/main/kotlin/io/unom/punktfunk/kit/security/KnownHostStore.kt b/clients/android/kit/src/main/kotlin/io/unom/punktfunk/kit/security/KnownHostStore.kt index a651497..5ab3f68 100644 --- a/clients/android/kit/src/main/kotlin/io/unom/punktfunk/kit/security/KnownHostStore.kt +++ b/clients/android/kit/src/main/kotlin/io/unom/punktfunk/kit/security/KnownHostStore.kt @@ -74,6 +74,16 @@ class KnownHostStore(context: Context) { save(h.copy(name = newName)) } + /** + * Edit a saved host, RE-KEYING if the address or port changed (the pref key IS `address:port`, so + * a plain [save] would otherwise leave a stale record under the old key). The caller passes an + * [updated] copy that preserves `fpHex`/`paired` (and sets `mac` from the edit form). + */ + fun update(oldAddress: String, oldPort: Int, updated: KnownHost) { + if (oldAddress != updated.address || oldPort != updated.port) remove(oldAddress, oldPort) + save(updated) + } + /** All trusted hosts, name-sorted — backs the saved-hosts list. */ fun all(): List = prefs.all.values.mapNotNull { (it as? String)?.let(::parse) }.sortedBy { it.name.lowercase() } @@ -89,4 +99,22 @@ class KnownHostStore(context: Context) { mac = j.optString("mac", "").split(",").map { it.trim() }.filter { it.isNotEmpty() }, ) }.getOrNull() + + companion object { + /** + * Parse a free-typed Wake-on-LAN field into normalized `aa:bb:cc:dd:ee:ff` entries (comma / + * space / newline separated). Anything that isn't six colon-separated hex octets is dropped; + * an empty result clears the host's MAC. Mirrors the Apple client's `AddHostSheet.parseMacs`. + */ + fun parseMacs(s: String): List = s + .split(',', ';', ' ', '\n', '\t') + .map { it.trim().lowercase() } + .filter { m -> + // Exactly six octets, each two literal hex digits. (Not toIntOrNull(16) — that accepts + // a leading +/- sign, so "aa:bb:cc:dd:ee:-1" would wrongly pass.) + m.split(":").let { o -> + o.size == 6 && o.all { it.length == 2 && it.all { c -> c in '0'..'9' || c in 'a'..'f' } } + } + } + } } diff --git a/clients/android/kit/src/test/kotlin/io/unom/punktfunk/kit/security/KnownHostStoreTest.kt b/clients/android/kit/src/test/kotlin/io/unom/punktfunk/kit/security/KnownHostStoreTest.kt new file mode 100644 index 0000000..1b19e58 --- /dev/null +++ b/clients/android/kit/src/test/kotlin/io/unom/punktfunk/kit/security/KnownHostStoreTest.kt @@ -0,0 +1,33 @@ +package io.unom.punktfunk.kit.security + +import org.junit.Assert.assertEquals +import org.junit.Test + +/** Unit tests for the pure MAC-parsing helper backing the host edit form. */ +class KnownHostStoreTest { + @Test + fun parsesAndNormalizesSingleMac() { + assertEquals(listOf("aa:bb:cc:dd:ee:ff"), KnownHostStore.parseMacs("AA:BB:CC:DD:EE:FF")) + } + + @Test + fun parsesMultipleSeparators() { + val expected = listOf("aa:bb:cc:dd:ee:ff", "11:22:33:44:55:66") + assertEquals(expected, KnownHostStore.parseMacs("aa:bb:cc:dd:ee:ff, 11:22:33:44:55:66")) + assertEquals(expected, KnownHostStore.parseMacs("aa:bb:cc:dd:ee:ff 11:22:33:44:55:66")) + assertEquals(expected, KnownHostStore.parseMacs("aa:bb:cc:dd:ee:ff\n11:22:33:44:55:66")) + } + + @Test + fun dropsMalformedEntries() { + // Not six octets / bad hex / wrong width are all dropped; an empty field clears the MAC. + assertEquals(emptyList(), KnownHostStore.parseMacs("")) + assertEquals(emptyList(), KnownHostStore.parseMacs("not-a-mac")) + assertEquals(emptyList(), KnownHostStore.parseMacs("aa:bb:cc:dd:ee")) // 5 octets + assertEquals(emptyList(), KnownHostStore.parseMacs("gg:bb:cc:dd:ee:ff")) // non-hex + assertEquals(emptyList(), KnownHostStore.parseMacs("aaa:bb:cc:dd:ee:ff")) // wrong width + assertEquals(emptyList(), KnownHostStore.parseMacs("aa:bb:cc:dd:ee:-1")) // signed octet + assertEquals(emptyList(), KnownHostStore.parseMacs("+a:-b:+c:-d:+e:-f")) // signed octets + assertEquals(listOf("aa:bb:cc:dd:ee:ff"), KnownHostStore.parseMacs("junk, aa:bb:cc:dd:ee:ff")) + } +} diff --git a/clients/android/native/Cargo.toml b/clients/android/native/Cargo.toml index 6d76e39..a4f9272 100644 --- a/clients/android/native/Cargo.toml +++ b/clients/android/native/Cargo.toml @@ -34,7 +34,11 @@ android_logger = "0.14" # NDK bindings. "media" = AMediaCodec/ANativeWindow (video); "audio" = AAudio (audio playback). # Pure-Rust FFI to libmediandk/libnativewindow/libaaudio — no C++/libc++_shared to bundle. Decode + # audio run entirely in Rust on native threads (the "no async on the hot path" invariant). -ndk = { version = "0.9", features = ["media", "audio", "nativewindow", "api-level-31"] } +# api-level-28 matches the app's minSdk floor (Android 9). AAudio (26), AMediaCodec (21) and +# ANativeWindow_setBuffersDataSpace (28) are all ≤28; the one API-30 call we make +# (ANativeWindow_setFrameRate) is dlsym-resolved at runtime (see decode::try_set_frame_rate), not +# linked, so the .so still loads on API 28/29. +ndk = { version = "0.9", features = ["media", "audio", "nativewindow", "api-level-28"] } # setpriority/gettid to raise the decode thread toward URGENT_DISPLAY (see decode::boost_thread_priority). libc = "0.2" # Opus decode for the host→client audio plane (0xC9: 48 kHz stereo, 5 ms frames). Same crate the diff --git a/clients/android/native/src/decode.rs b/clients/android/native/src/decode.rs index 3d934c7..77b9f62 100644 --- a/clients/android/native/src/decode.rs +++ b/clients/android/native/src/decode.rs @@ -12,11 +12,12 @@ use ndk::media::media_codec::{ OutputBuffer, }; use ndk::media::media_format::MediaFormat; -use ndk::native_window::{FrameRateCompatibility, NativeWindow}; +use ndk::native_window::NativeWindow; use punktfunk_core::client::NativeClient; use punktfunk_core::error::PunktfunkError; use punktfunk_core::session::Frame; use std::collections::VecDeque; +use std::ffi::c_void; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use std::time::{Duration, Instant}; @@ -113,11 +114,13 @@ pub fn run( mode.height ); // Tell the display the stream's refresh so Android can pick a matching display mode and align - // vsync (no 60-in-120 judder on high-refresh panels). minSdk 31 ≥ API 30, so the underlying - // ANativeWindow_setFrameRate is always present; non-fatal if the platform declines. - if let Err(e) = window.set_frame_rate(mode.refresh_hz as f32, FrameRateCompatibility::Default) { - log::warn!( - "decode: set_frame_rate({} Hz) failed (non-fatal): {e}", + // vsync (no 60-in-120 judder on high-refresh panels). `ANativeWindow_setFrameRate` is NDK API 30, + // above our API-28 floor, so we resolve it at runtime (see `try_set_frame_rate`) rather than link + // it — a hard import would stop `libpunktfunk_android.so` loading at all on API 28/29. Absent + // there ⇒ we simply skip the hint (non-fatal; the stream renders fine without it). + if mode.refresh_hz > 0 && !try_set_frame_rate(&window, mode.refresh_hz as f32) { + log::debug!( + "decode: set_frame_rate({} Hz) unavailable/declined (non-fatal)", mode.refresh_hz ); } @@ -340,6 +343,32 @@ fn boost_thread_priority() { } } +/// `ANativeWindow_setFrameRate` (NDK **API 30**) resolved from `libandroid.so` at runtime, so the lib +/// still loads on our API-28 floor — a hard import of a >floor symbol makes `dlopen`/`System.load` +/// fail on every API-28/29 device, even where this path is never hit. Mirrors the dlsym approach in +/// [`crate::adpf`]. Returns `true` when the platform accepted the hint; `false` on API < 30 (symbol +/// absent) or when the platform declined. `compatibility` is fixed to the DEFAULT (0) policy. +fn try_set_frame_rate(window: &NativeWindow, frame_rate: f32) -> bool { + // int32_t ANativeWindow_setFrameRate(ANativeWindow*, float frameRate, int8_t compatibility) + type SetFrameRateFn = unsafe extern "C" fn(*mut c_void, f32, i8) -> i32; + // SAFETY: `dlopen` of the always-mapped `libandroid.so` (only bumps its refcount; never closed — + // process-lifetime handle). `dlsym` returns null when the symbol is absent (device API < 30), + // checked before transmuting the non-null pointer to its fn-pointer type. `window.ptr()` is the + // live `ANativeWindow` this `NativeWindow` owns for the call's duration. + unsafe { + let lib = libc::dlopen(c"libandroid.so".as_ptr(), libc::RTLD_NOW); + if lib.is_null() { + return false; + } + let sym = libc::dlsym(lib, c"ANativeWindow_setFrameRate".as_ptr()); + if sym.is_null() { + return false; // device API < 30 — no per-surface frame-rate hint + } + let set_frame_rate = std::mem::transmute::<*mut c_void, SetFrameRateFn>(sym); + set_frame_rate(window.ptr().as_ptr().cast(), frame_rate, 0) == 0 + } +} + /// Try to copy one access unit into a codec input buffer and queue it, without blocking. Returns /// `false` only on `TryAgainLater` (no input buffer free) — the caller keeps the AU pending and /// retries; a hard dequeue/queue error counts as consumed (retrying can't salvage the AU, and