Files
punktfunk/clients/android/app
enricobuehler b63f9f4ac0 feat(client/android): punktfunk:// links open a stream
Android was the last client with no URL door at all: no VIEW intent filter,
no `onNewIntent`, no parser. Now a Playnite entry, an OS shortcut, a Stream
Deck macro or a wiki link can open a stream on a host this device already
trusts.

The parser is a PORT, not a new design. `clients/shared/deeplink-vectors.json`
is the cross-language contract, and the Kotlin suite runs the same 44 cases
the Rust one does — including every refusal code — so three parsers cannot
drift into three different security postures. It is resolved from the shared
path rather than copied, because a copy would be a fourth contract free to go
stale. Strict percent-decoding with a REPORTING UTF-8 decoder, so `%FF` is a
refusal rather than a U+FFFD that survives into a log line or a filename.

The routing lives in `ConnectScreen` because that is what owns the connect
path — trust decisions, the local-network grant, wake-and-retry — and a link
must go THROUGH all of it, never around it. The rules are absolute and each
one is a branch you can point at: a known, pinned host does exactly what
tapping its card does; an unknown or never-pinned one gets the confirmation
sheet, from which the normal pairing flow proceeds under the user's eyes; an
`fp=` that contradicts the stored pin is a hard refusal; an ambiguous host
name or a profile this device doesn't have refuses with a notice naming what
failed, because a "Work" shortcut streaming with the wrong settings is worse
than an error; a link arriving mid-stream never preempts it (pointing at the
host already being streamed is a no-op — the intent has already brought the
app forward, which is what focusing it means). `wake` and `browse` parse, and
are refused with a notice rather than silently connecting.

`launch=` and `profile=` ride the whole path, including through a trust
decision: a link to a host that still needs pairing keeps its game and its
profile across the confirmation instead of quietly landing on a plain desktop
session.

`launchMode` stays `standard` and the `configChanges` set is untouched — its
`keyboard` entry is what keeps an SC2 claim from killing a running stream.
The VIEW intent is read in both `onCreate` and `onNewIntent`, which is what
that launch mode requires.
2026-07-29 12:17:18 +02:00
..