fix(android): launch games from the library
The library browser was browse-only — the A button (and a tap) did nothing. Wire it to connect + boot straight into the selected title: thread a `launch` id (the store-qualified library id `steam:<appid>` / `custom:<id>`) through nativeConnect → NativeClient's Hello.launch (was hardcoded None), add a shared connectToHost() the ConnectScreen and the library launcher both use, and have LibraryScreen dial the host with launch=game.id on A / tap — with a launching overlay + an "A Launch" hint. Verified: native compiles (cargo-ndk arm64), app+kit Kotlin compiles (gradle, 3 ABIs). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -208,6 +208,8 @@ fun GamepadShell(
|
|||||||
GamepadScreen.Library -> libraryHost?.let { host ->
|
GamepadScreen.Library -> libraryHost?.let { host ->
|
||||||
LibraryScreen(
|
LibraryScreen(
|
||||||
host = host,
|
host = host,
|
||||||
|
settings = settings,
|
||||||
|
onLaunched = onConnected,
|
||||||
onBack = { screen = GamepadScreen.Home; libraryHost = null },
|
onBack = { screen = GamepadScreen.Home; libraryHost = null },
|
||||||
navActive = s == screen,
|
navActive = s == screen,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -63,9 +63,6 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
/** Handshake budget for a normal connect (the prior hardcoded value, now passed explicitly). */
|
|
||||||
private const val CONNECT_TIMEOUT_MS = 10_000
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handshake budget for the no-PIN "request access" connect. Must exceed the host's approval-park
|
* Handshake budget for the no-PIN "request access" connect. Must exceed the host's approval-park
|
||||||
* window (~180 s) so a slow operator approval still lands on this same parked connection rather than
|
* window (~180 s) so a slow operator approval still lands on this same parked connection rather than
|
||||||
@@ -181,25 +178,10 @@ fun ConnectScreen(
|
|||||||
// it survives a DHCP address change; else by address:port). Mirrors the Apple client.
|
// it survives a DHCP address change; else by address:port). Mirrors the Apple client.
|
||||||
val discoveredUnsaved = discovered.filter { dh -> savedHosts.none { it.matches(dh) } }
|
val discoveredUnsaved = discovered.filter { dh -> savedHosts.none { it.matches(dh) } }
|
||||||
|
|
||||||
// The one place the full nativeConnect is issued (shared by the normal connect and the
|
// Issue the native connect (shared by the normal connect and the request-access path). A plain
|
||||||
// request-access path), including the HDR/gamepad derivation both need.
|
// desktop connect (no library launch) — the library launcher calls [connectToHost] with an id.
|
||||||
suspend fun connectNative(id: ClientIdentity, targetHost: String, targetPort: Int, pinHex: String, timeoutMs: Int): Long {
|
suspend fun connectNative(id: ClientIdentity, targetHost: String, targetPort: Int, pinHex: String, timeoutMs: Int): Long =
|
||||||
// Advertise HDR only when the user enabled it AND this device's display can present it
|
connectToHost(context, settings, id, targetHost, targetPort, pinHex, launch = null, timeoutMs = timeoutMs)
|
||||||
// (else the host sends a proper SDR stream rather than PQ the panel would mis-tone-map).
|
|
||||||
val hdrEnabled = settings.hdrEnabled && displaySupportsHdr(context)
|
|
||||||
// "Automatic" resolves to a concrete pad type from the connected controller's VID/PID
|
|
||||||
// (Android exposes no controller-type enum) — parity with the Linux/Apple clients. An
|
|
||||||
// explicit choice is passed through unchanged.
|
|
||||||
val gamepadPref = Gamepad.resolvePref(settings.gamepad)
|
|
||||||
return withContext(Dispatchers.IO) {
|
|
||||||
NativeBridge.nativeConnect(
|
|
||||||
targetHost, targetPort, w, h, hz,
|
|
||||||
id.certPem, id.privateKeyPem, pinHex,
|
|
||||||
settings.bitrateKbps, settings.compositor, gamepadPref,
|
|
||||||
hdrEnabled, settings.audioChannels, settings.preferredCodec(), timeoutMs,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The actual dial (identity already ready). On a TOFU connect (pinHex null), pin the fingerprint
|
// 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
|
// the host presented (as an unpaired known host) so the next connect goes straight through and it
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package io.unom.punktfunk
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import io.unom.punktfunk.kit.Gamepad
|
||||||
|
import io.unom.punktfunk.kit.NativeBridge
|
||||||
|
import io.unom.punktfunk.kit.security.ClientIdentity
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
/** Handshake budget for a normal / library-launch connect (not the long request-access park). */
|
||||||
|
const val CONNECT_TIMEOUT_MS = 10_000
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The one place [NativeBridge.nativeConnect] is assembled — shared by [ConnectScreen] and the library
|
||||||
|
* launcher ([LibraryScreen]). Derives the mode / HDR / gamepad settings the host needs from
|
||||||
|
* [settings]. [pinHex] is the pinned fingerprint (empty ⇒ TOFU). [launch] is a store-qualified library
|
||||||
|
* id (`steam:<appid>` / `custom:<id>`) to boot straight into a game, or `null` for the desktop.
|
||||||
|
* Returns the session handle, or `0` on failure. Call off the main thread.
|
||||||
|
*/
|
||||||
|
suspend fun connectToHost(
|
||||||
|
context: Context,
|
||||||
|
settings: Settings,
|
||||||
|
identity: ClientIdentity,
|
||||||
|
host: String,
|
||||||
|
port: Int,
|
||||||
|
pinHex: String,
|
||||||
|
launch: String?,
|
||||||
|
timeoutMs: Int = CONNECT_TIMEOUT_MS,
|
||||||
|
): Long {
|
||||||
|
// Advertise HDR only when the user enabled it AND this device's display can present it (else the
|
||||||
|
// host sends a proper SDR stream rather than PQ the panel would mis-tone-map).
|
||||||
|
val (w, h, hz) = settings.effectiveMode(context)
|
||||||
|
val hdrEnabled = settings.hdrEnabled && displaySupportsHdr(context)
|
||||||
|
// "Automatic" resolves to a concrete pad type from the connected controller's VID/PID.
|
||||||
|
val gamepadPref = Gamepad.resolvePref(settings.gamepad)
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
NativeBridge.nativeConnect(
|
||||||
|
host, port, w, h, hz,
|
||||||
|
identity.certPem, identity.privateKeyPem, pinHex,
|
||||||
|
settings.bitrateKbps, settings.compositor, gamepadPref,
|
||||||
|
hdrEnabled, settings.audioChannels, settings.preferredCodec(), timeoutMs,
|
||||||
|
launch,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
package io.unom.punktfunk
|
package io.unom.punktfunk
|
||||||
|
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
@@ -57,6 +59,7 @@ import io.unom.punktfunk.kit.library.GameEntry
|
|||||||
import io.unom.punktfunk.kit.library.LibraryClient
|
import io.unom.punktfunk.kit.library.LibraryClient
|
||||||
import io.unom.punktfunk.kit.library.LibraryResult
|
import io.unom.punktfunk.kit.library.LibraryResult
|
||||||
import io.unom.punktfunk.kit.library.mtlsHttpClient
|
import io.unom.punktfunk.kit.library.mtlsHttpClient
|
||||||
|
import io.unom.punktfunk.kit.security.ClientIdentity
|
||||||
import io.unom.punktfunk.kit.security.IdentityStore
|
import io.unom.punktfunk.kit.security.IdentityStore
|
||||||
import io.unom.punktfunk.kit.security.KnownHost
|
import io.unom.punktfunk.kit.security.KnownHost
|
||||||
import io.unom.punktfunk.kit.security.obtainIdentity
|
import io.unom.punktfunk.kit.security.obtainIdentity
|
||||||
@@ -73,17 +76,27 @@ import kotlinx.coroutines.withContext
|
|||||||
|
|
||||||
private sealed class LibState {
|
private sealed class LibState {
|
||||||
object Loading : LibState()
|
object Loading : LibState()
|
||||||
data class Ready(val games: List<GameEntry>, val loader: ImageLoader) : LibState()
|
// Carries the client identity so a launch can dial the host over the same pinned mTLS trust.
|
||||||
|
data class Ready(val games: List<GameEntry>, val loader: ImageLoader, val identity: ClientIdentity) : LibState()
|
||||||
data class Message(val text: String) : LibState() // unauthorized / empty / error
|
data class Message(val text: String) : LibState() // unauthorized / empty / error
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LibraryScreen(host: KnownHost, onBack: () -> Unit, navActive: Boolean = true) {
|
fun LibraryScreen(
|
||||||
|
host: KnownHost,
|
||||||
|
settings: Settings,
|
||||||
|
onLaunched: (Long) -> Unit,
|
||||||
|
onBack: () -> Unit,
|
||||||
|
navActive: Boolean = true,
|
||||||
|
) {
|
||||||
BackHandler(onBack = onBack)
|
BackHandler(onBack = onBack)
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
val hazeState = remember { HazeState() }
|
val hazeState = remember { HazeState() }
|
||||||
val landscape = LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE
|
val landscape = LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE
|
||||||
var state by remember { mutableStateOf<LibState>(LibState.Loading) }
|
var state by remember { mutableStateOf<LibState>(LibState.Loading) }
|
||||||
|
// A launch (connect) in flight: shows an overlay + gates the pad so a second press can't dial twice.
|
||||||
|
var launching by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
LaunchedEffect(host.address, host.port, host.fpHex) {
|
LaunchedEffect(host.address, host.port, host.fpHex) {
|
||||||
state = LibState.Loading
|
state = LibState.Loading
|
||||||
@@ -101,7 +114,7 @@ fun LibraryScreen(host: KnownHost, onBack: () -> Unit, navActive: Boolean = true
|
|||||||
LibState.Message("No games found on this host.")
|
LibState.Message("No games found on this host.")
|
||||||
} else {
|
} else {
|
||||||
val client = mtlsHttpClient(id.certPem, id.privateKeyPem, host.address, host.fpHex)
|
val client = mtlsHttpClient(id.certPem, id.privateKeyPem, host.address, host.fpHex)
|
||||||
LibState.Ready(res.games, ImageLoader.Builder(context).okHttpClient(client).build())
|
LibState.Ready(res.games, ImageLoader.Builder(context).okHttpClient(client).build(), id)
|
||||||
}
|
}
|
||||||
is LibraryResult.Unauthorized -> LibState.Message(res.message)
|
is LibraryResult.Unauthorized -> LibState.Message(res.message)
|
||||||
is LibraryResult.Error -> LibState.Message(res.message)
|
is LibraryResult.Error -> LibState.Message(res.message)
|
||||||
@@ -118,11 +131,45 @@ fun LibraryScreen(host: KnownHost, onBack: () -> Unit, navActive: Boolean = true
|
|||||||
when (val s = state) {
|
when (val s = state) {
|
||||||
is LibState.Loading -> LoadingState()
|
is LibState.Loading -> LoadingState()
|
||||||
is LibState.Message -> MessageState(s.text)
|
is LibState.Message -> MessageState(s.text)
|
||||||
is LibState.Ready -> Coverflow(s.games, s.loader, navActive)
|
is LibState.Ready -> Coverflow(s.games, s.loader, navActive && !launching) { game ->
|
||||||
|
if (!launching) {
|
||||||
|
launching = true
|
||||||
|
scope.launch {
|
||||||
|
// Dial the host over the same pinned mTLS trust, booting straight
|
||||||
|
// into this title (the host resolves `launch` = its library id).
|
||||||
|
val handle = connectToHost(
|
||||||
|
context, settings, s.identity,
|
||||||
|
host.address, host.port, host.fpHex, launch = game.id,
|
||||||
|
)
|
||||||
|
launching = false
|
||||||
|
if (handle != 0L) onLaunched(handle)
|
||||||
|
else Toast.makeText(
|
||||||
|
context,
|
||||||
|
"Launch failed — check the host and try again.",
|
||||||
|
Toast.LENGTH_LONG,
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Launching overlay — the connect + host-side game boot takes a moment; block the pad while it runs.
|
||||||
|
if (launching) {
|
||||||
|
Box(
|
||||||
|
Modifier.fillMaxSize().background(Color.Black.copy(alpha = 0.6f)),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(14.dp),
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator(color = Color.White)
|
||||||
|
Text("Launching…", color = Color.White, style = MaterialTheme.typography.bodyLarge)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
// Floating legend at the shared spot — same landscape-aware inset as every other console
|
// 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).
|
// screen (ignore the safe area in landscape, where the bottom edge isn't a tap target).
|
||||||
Box(
|
Box(
|
||||||
@@ -130,7 +177,13 @@ fun LibraryScreen(host: KnownHost, onBack: () -> Unit, navActive: Boolean = true
|
|||||||
.then(if (landscape) Modifier else Modifier.systemBarsPadding())
|
.then(if (landscape) Modifier else Modifier.systemBarsPadding())
|
||||||
.padding(ConsoleLegendInset),
|
.padding(ConsoleLegendInset),
|
||||||
) {
|
) {
|
||||||
GamepadHintBar(listOf(PadGlyph.hint('B', "Close", onClick = onBack)), hazeState = hazeState)
|
GamepadHintBar(
|
||||||
|
buildList {
|
||||||
|
if (state is LibState.Ready) add(PadGlyph.hint('A', "Launch"))
|
||||||
|
add(PadGlyph.hint('B', "Close", onClick = onBack))
|
||||||
|
},
|
||||||
|
hazeState = hazeState,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -155,7 +208,12 @@ private fun MessageState(text: String) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun Coverflow(games: List<GameEntry>, loader: ImageLoader, navActive: Boolean) {
|
private fun Coverflow(
|
||||||
|
games: List<GameEntry>,
|
||||||
|
loader: ImageLoader,
|
||||||
|
navActive: Boolean,
|
||||||
|
onLaunch: (GameEntry) -> Unit,
|
||||||
|
) {
|
||||||
BoxWithConstraints(Modifier.fillMaxSize()) {
|
BoxWithConstraints(Modifier.fillMaxSize()) {
|
||||||
// Fit a 2:3 poster into the height the detail line leaves; clamp so it never dwarfs the screen.
|
// 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 coverHeight = (maxHeight * 0.72f).coerceAtMost(360.dp)
|
||||||
@@ -167,16 +225,15 @@ private fun Coverflow(games: List<GameEntry>, loader: ImageLoader, navActive: Bo
|
|||||||
LaunchedEffect(pagerState.settledPage) { navTarget = pagerState.settledPage }
|
LaunchedEffect(pagerState.settledPage) { navTarget = pagerState.settledPage }
|
||||||
val current = games.getOrNull(navTarget)
|
val current = games.getOrNull(navTarget)
|
||||||
|
|
||||||
// Controller nav: the pad drives the coverflow (it wasn't captured before). Left/right steps a
|
// Controller nav: the pad drives the coverflow. Left/right steps a coalesced target the pager
|
||||||
// coalesced target the pager chases; A is reserved for launch (browse-only for now); B closes
|
// chases; A launches the centred title; B closes via the screen's BackHandler.
|
||||||
// via the screen's BackHandler.
|
|
||||||
GamepadNavEffect(
|
GamepadNavEffect(
|
||||||
active = navActive && games.isNotEmpty(),
|
active = navActive && games.isNotEmpty(),
|
||||||
onMove = { dir ->
|
onMove = { dir ->
|
||||||
val t = (navTarget + dir).coerceIn(0, games.lastIndex)
|
val t = (navTarget + dir).coerceIn(0, games.lastIndex)
|
||||||
if (t != navTarget) { navTarget = t; scope.launch { pagerState.animateScrollToPage(t) } }
|
if (t != navTarget) { navTarget = t; scope.launch { pagerState.animateScrollToPage(t) } }
|
||||||
},
|
},
|
||||||
onActivate = { /* launch a title — browse-only for now */ },
|
onActivate = { games.getOrNull(navTarget)?.let(onLaunch) },
|
||||||
)
|
)
|
||||||
|
|
||||||
Column(Modifier.fillMaxSize(), verticalArrangement = Arrangement.Center) {
|
Column(Modifier.fillMaxSize(), verticalArrangement = Arrangement.Center) {
|
||||||
@@ -198,6 +255,11 @@ private fun Coverflow(games: List<GameEntry>, loader: ImageLoader, navActive: Bo
|
|||||||
.zIndex(-d) // centred cover on top, neighbours stacked behind
|
.zIndex(-d) // centred cover on top, neighbours stacked behind
|
||||||
.width(coverWidth)
|
.width(coverWidth)
|
||||||
.height(coverHeight)
|
.height(coverHeight)
|
||||||
|
// Touch: tap the centred cover to launch it; tap a neighbour to bring it centre.
|
||||||
|
.clickable {
|
||||||
|
if (page == pagerState.currentPage) onLaunch(games[page])
|
||||||
|
else scope.launch { pagerState.animateScrollToPage(page) }
|
||||||
|
}
|
||||||
.graphicsLayer {
|
.graphicsLayer {
|
||||||
// Centre at full size; EVERY neighbour settles to one size, so an even pitch
|
// 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 —
|
// yields even VISUAL gaps. (A progressive shrink made the outer gaps grow —
|
||||||
|
|||||||
@@ -51,6 +51,9 @@ object NativeBridge {
|
|||||||
/** Preferred video codec as a `quic::CODEC_*` bit (`0` = auto). Soft — the host falls back. */
|
/** Preferred video codec as a `quic::CODEC_*` bit (`0` = auto). Soft — the host falls back. */
|
||||||
preferredCodec: Int,
|
preferredCodec: Int,
|
||||||
timeoutMs: Int,
|
timeoutMs: Int,
|
||||||
|
/** Store-qualified library id (`steam:<appid>` / `custom:<id>`) to boot straight into a game,
|
||||||
|
* or `null`/empty for a plain desktop connect. Rides the Hello as `launch`. */
|
||||||
|
launch: String?,
|
||||||
): Long
|
): Long
|
||||||
|
|
||||||
/** 64-hex SHA-256 of the cert the host presented on [handle]; valid after a successful connect. */
|
/** 64-hex SHA-256 of the cert the host presented on [handle]; valid after a successful connect. */
|
||||||
|
|||||||
@@ -33,7 +33,8 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeGenerateIde
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// `NativeBridge.nativeConnect(host, port, w, h, hz, certPem, keyPem, pinHex, bitrateKbps,
|
/// `NativeBridge.nativeConnect(host, port, w, h, hz, certPem, keyPem, pinHex, bitrateKbps,
|
||||||
/// compositorPref, gamepadPref, hdrEnabled, audioChannels, preferredCodec, timeoutMs): Long`.
|
/// compositorPref, gamepadPref, hdrEnabled, audioChannels, preferredCodec, timeoutMs, launch): Long`.
|
||||||
|
/// `launch` (empty ⇒ none) is a store-qualified library id to boot straight into a game.
|
||||||
/// `certPem`/`keyPem` empty = anonymous, else presented as the persistent identity. `pinHex` empty
|
/// `certPem`/`keyPem` empty = anonymous, else presented as the persistent identity. `pinHex` empty
|
||||||
/// = TOFU (read `nativeHostFingerprint` after), else 64-hex SHA-256 to pin the host (mismatch → 0).
|
/// = TOFU (read `nativeHostFingerprint` after), else 64-hex SHA-256 to pin the host (mismatch → 0).
|
||||||
/// `bitrateKbps` 0 = host default. `compositorPref`/`gamepadPref` are `CompositorPref`/`GamepadPref`
|
/// `bitrateKbps` 0 = host default. `compositorPref`/`gamepadPref` are `CompositorPref`/`GamepadPref`
|
||||||
@@ -63,6 +64,7 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeConnect<'lo
|
|||||||
audio_channels: jint,
|
audio_channels: jint,
|
||||||
preferred_codec: jint,
|
preferred_codec: jint,
|
||||||
timeout_ms: jint,
|
timeout_ms: jint,
|
||||||
|
launch: JString<'local>,
|
||||||
) -> jlong {
|
) -> jlong {
|
||||||
let host: String = match env.get_string(&host) {
|
let host: String = match env.get_string(&host) {
|
||||||
Ok(s) => s.into(),
|
Ok(s) => s.into(),
|
||||||
@@ -74,6 +76,13 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeConnect<'lo
|
|||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let key: String = env.get_string(&key_pem).map(Into::into).unwrap_or_default();
|
let key: String = env.get_string(&key_pem).map(Into::into).unwrap_or_default();
|
||||||
let pin_hex: String = env.get_string(&pin_hex).map(Into::into).unwrap_or_default();
|
let pin_hex: String = env.get_string(&pin_hex).map(Into::into).unwrap_or_default();
|
||||||
|
// A store-qualified library id (`steam:<appid>` / `custom:<id>`) to boot straight into a game;
|
||||||
|
// null / empty ⇒ None (a plain desktop connect). Rides the Hello as `launch`.
|
||||||
|
let launch: Option<String> = env
|
||||||
|
.get_string(&launch)
|
||||||
|
.map(Into::into)
|
||||||
|
.ok()
|
||||||
|
.filter(|s: &String| !s.is_empty());
|
||||||
|
|
||||||
let identity: Option<(String, String)> = if cert.is_empty() || key.is_empty() {
|
let identity: Option<(String, String)> = if cert.is_empty() || key.is_empty() {
|
||||||
None
|
None
|
||||||
@@ -124,7 +133,7 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeConnect<'lo
|
|||||||
// + the soft `preferred_codec` and echoes it in `connector.codec`, which drives the mime below.
|
// + the soft `preferred_codec` and echoes it in `connector.codec`, which drives the mime below.
|
||||||
punktfunk_core::quic::CODEC_H264 | punktfunk_core::quic::CODEC_HEVC,
|
punktfunk_core::quic::CODEC_H264 | punktfunk_core::quic::CODEC_HEVC,
|
||||||
preferred_codec.clamp(0, u8::MAX as jint) as u8,
|
preferred_codec.clamp(0, u8::MAX as jint) as u8,
|
||||||
None, // launch: default app
|
launch, // a store-qualified library id to boot into a game, or None for the desktop
|
||||||
pin, // Some → Crypto on host-fp mismatch
|
pin, // Some → Crypto on host-fp mismatch
|
||||||
identity, // owned (cert, key) PEM, or None (anonymous)
|
identity, // owned (cert, key) PEM, or None (anonymous)
|
||||||
// Handshake budget from Kotlin: ~10 s for a normal connect, ~185 s for "request access"
|
// Handshake budget from Kotlin: ~10 s for a normal connect, ~185 s for "request access"
|
||||||
|
|||||||
Reference in New Issue
Block a user