Conversation
This was referenced Oct 5, 2026
kivtxs
force-pushed
the
feat/android-wifi-direct-groups
branch
4 times, most recently
from
October 6, 2026 04:37
6dc721d to
01afaf0
Compare
…a system dialog Closes the residual #465 left open ("the Android manager does not form a group itself"). With wifiDirect.autoAccept on Android 10+: - Devices of one app find each other over Wi-Fi P2P DNS-SD: the stream chapter's _offlineprotocol._tcp record (txtvers, addr) plus `app` (a tag of the app id) and `net` (the group name, while owning one). - The lowest address creates a group under a name derived from its address; the others join it with a passphrase derived from the app id. Joining by credentials shows no dialog on either phone, unlike connect() by device address, whose invitation a phone in a pocket never answers. - The query names the instance, because a query by type returns only the PTR record and never reaches the TXT listener. A group owner answers no service discovery query, so joiners target the name the lowest peer will create, and an owner whose group stays empty for 45 s dissolves it so it can be found again. The rules are a pure object (WifiDirectGroupFormation) with 20 JVM tests. Off by default; groupOwnerIntent stays unused. Measured on an Android 13 and an Android 15 phone: launch to proved stream in 26-55 s over five clean starts, chat 3/3 and 4/4 each way at ~100 ms.
Turning Wi-Fi P2P off reported the slot down; turning it on reported nothing, restarted no discovery and, with group formation on, left the framework's dropped service request and record unregistered, so every later discoverServices failed with NO_SERVICE_REQUESTS. An app started with Wi-Fi off never got Wi-Fi Direct. The manager tracks whether it last reported the layer up, reports it up again when P2P returns (a repeated broadcast is a no-op), restarts peer discovery, re-registers formation's service request and record, and re-registers the request on NO_SERVICE_REQUESTS. Formation pauses while the layer is down instead of failing BUSY. On the phones: app started with Wi-Fi off, then Wi-Fi on: group formed and streams proved in 73 s; Wi-Fi off and on mid-session on one phone: re-formed in 19 s.
…tials The first formation design named the group after its owner, so a joiner had to hear the owner's DNS-SD record before it could join. On two phones that failed: a device that owns or is joining a group answers no service discovery query, and the two often heard each other minutes apart or never. Of nine clean starts, two never formed and two took about two minutes. - The group's name and passphrase derive from the app id alone, so a joiner needs nothing from the owner. The `net` record entry is gone. - The lowest address heard creates; others join, and take over creating after three joins found no group (the lower peer may never hear them). - A device that heard no record joins only as a paced probe (once a minute) when a group owner is nearby: Wi-Fi Direct televisions are owners too, and joining on every step kept both phones deaf. - A join attempt is cancelled after 15 s (it must scan, and the supplicant retries a rejected association) and attempts are 30 s apart, so a joiner stays discoverable half the time. - An owner whose group stays empty for a randomised 30-60 s dissolves it and joins twice before it may create again, so groups formed at once merge. Eight clean starts on an Android 13 and an Android 15 phone next to two Wi-Fi Direct televisions: all formed, 18-225 s, median about a minute, no system dialog. 18 JVM tests cover the rules, including the asymmetric cases.
bahdotsh
force-pushed
the
feat/android-wifi-direct-groups
branch
from
October 6, 2026 05:18
01afaf0 to
c3e71d5
Compare
… it on stop
Calling enableTransport('wifiDirect') with no config, which the
integration guide tells apps to do once a permission is granted,
turned group formation *off*. A config that didn't name autoAccept
was read as false.
Then it got worse. stop() decided whether to clean up by checking
the flag that had just been cleared, so it skipped removing the
group this run had created. The device kept owning an empty
app-named group, and an owner answers no service discovery, so its
peers could no longer find it. Confusion ensues.
So a config that doesn't name the key now keeps the current
setting, and it is applied after the stop. stop() undoes what the
run actually registered and created, no matter what the flag says
by then.
While at it:
- The join timeout is a named runnable that stop() removes. A
timer left over from the previous run could otherwise cancel the
next run's join and count a failure it never saw.
- A join the framework refuses outright (BUSY, say) now counts
toward the takeover. Before, a device whose joins kept failing
never got to create the group itself.
- A device alone stops working the radio as hard. Service
discovery backs off from 15 to 60 s while nothing is heard, and
the owner probe from one to four minutes. A Wi-Fi Direct printer
or TV is a group owner no probe will ever join, and probing one
every minute forever is not great. Both reset when a record, a
new owner or a new device shows up.
- The passphrase comment no longer claims a forger can't get in.
Anyone who knows the app id can compute the passphrase.
… risk The bridge doc and the CHANGELOG still described the first design: an eight-second join window (the code uses fifteen) and an unthrottled join toward any group owner (the code probes once a minute). These are the docs you're told to read before changing the rules, so they had better be right. The integration guide's example set autoAccept: true back when the option did nothing. Apps that copied it start forming groups on upgrade, and the CHANGELOG now says so instead of claiming nothing changes. groupOwnerIntent is marked deprecated everywhere it is still documented as working. The group passphrase is derived from the app id, which ships inside every copy of the app, so it is not a secret. Add R22 to the threat model: who can join or squat on the group, what forged records can steer, and why the preamble and MLS are the real protection. The spec paragraph points to it.
A Wi-Fi P2P group belongs to the system, not to the process that created it. So when the owner's app gets killed in the background, the group stays up with nobody listening on the stream port, and an owner answers no service discovery query, so nobody can tell. Every other device of the app then sees "a group owner nearby", probe-joins it by the derived credentials, and succeeds. Now it is in a group, so formation never runs again, and it redials a dead port once a minute for the rest of the session. Wi-Fi Direct is gone until somebody cycles Wi-Fi. The threat model claimed idle owners bound a squatter. A dead owner is never idle. It just isn't there. Two rules close it. A client that is in an app-named group leaves after three dials in a row at the top of its redial ladder prove nothing, and joins before creating again. Dials below the ceiling don't count, so an owner that is just restarting its app doesn't get abandoned. A group paired by hand in system settings is never left. And an app-named group this device owns is removed by stop() even if it was adopted at start rather than created in this run. No other app derives that name, and leaving it up is how a device ends up as a hidden, serverless owner in the first place. The leave decision lives in GroupOwnerRedial, a pure function, so it is pinned without a radio. While at it: a new peer or a new record now makes the next service discovery due immediately. Previously it only shortened the period *after* a wait that could already be 60 s out. Also pace the local record publish retry, which was the one formation call that would retry every 10 s against a framework that keeps refusing it.
The guide and the autoAccept doc comment said the group forms "within about a minute". The measurements say 18 to 225 s, with half the runs over 56 s. The median is about a minute. That is not the same claim. Say one to two minutes and give the measured range, as the CHANGELOG already does.
The previous commit taught a client to leave an app-named group whose owner proves nothing. It left, all right. Ten seconds later it joined the exact same dead group again. The reason is that every group of an app has the *same* name. A join by name and passphrase lands on whichever owner of that name the supplicant finds, and the only one in sight is the owner we just walked away from. The leave also set join-first, and the dead owner still counted as "an owner nearby", so both the join-first rule and the owner probe aimed straight back at it. Captured for three minutes, leave, captured again. For the whole session. The CHANGELOG and R22 said "forms again". It never did. So remember the owner we left, by device address, for five minutes: longer than an owner whose app comes back takes to dissolve its empty group. It no longer counts as an owner nearby, and while it is remembered every join names its target with setDeviceAddress. The config still carries the network name and passphrase, so it stays a join by credentials and shows no invitation. With no other owner in sight the join is counted as one that found nothing, so the takeover makes the device create its own group. The leave no longer sets join-first. Second hole in the same place: asking to leave also reset the count and *returned* before posting the redial. If removeGroup failed (BUSY during a group transition, say) the client sat in the dead group with no stream, no redial, no leave retry and no formation. Nothing would ever fire again. Keep redialling; a failed leave is asked again on the next unproved dial, and a Rust guard now pins that shape. While at it: - Recount an owner's clients on PEERS_CHANGED too. A client leaving may not change the owner's connection, and an owner that still thinks it has a client never dissolves. - Don't publish the record twice at start. The first step ran before the first publish answered and started a second clear-and-add chain alongside it. - An owner counts as new, and resets the probe backoff, only after five minutes out of the peer list. A television at the edge of range flickering in and out re-armed the one-minute probe every time. Not device-tested: that a targeted join by credentials shows no dialog, and that the peer list's device address is the one setDeviceAddress wants. If either is wrong the join fails and counts toward the takeover, which is the safe way to be wrong.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Rebased onto
mainafter #505 merged (squashed as d20654d). This PR now holds only its own three commits. Two conflicts came from your review edits to #505: the integration guide (kept your fuller Wi-Fi Direct bullet and added theautoAcceptone) and the CHANGELOG (kept your once-per-edgetransport_switchedwording). The uniffi suite (200 tests) and the Android JVM suite pass on the rebased branch.Closes the residual #465 left open: "The Android manager does not form a group itself; it joins one the system formed." Until now, two phones running an app could only use Wi-Fi Direct after a person paired them by hand in system settings. That also left the documented
autoAccept/groupOwnerIntentoptions doing nothing.What it does (opt-in:
wifiDirect: { enabled: true, autoAccept: true }, Android 10+)_offlineprotocol._tcprecord (txtvers=1,addr=), plusapp(a tag of the app id). The spec gets a paragraph on this use.DIRECT-op-<hash(appId)>) and passphrase derive from the app id alone. A joiner needs nothing from the owner. Other apps' devices never compute the passphrase, and it is never sent.WifiDirectGroupFormation.decide, a pure object every device runs):WifiP2pConfig.Builder().setNetworkName().setPassphrase(), which shows no dialog on either phone.connect()by device address would show the other phone an invitation that a phone in a pocket never answers. The passphrase only keeps other apps out; the identity preamble and MLS still protect the traffic.Radio behaviours that shaped it (found on the phones, written into the code and
docs/bridges/kotlin.md)offlineprotocol).Validation
On the phones (Infinix NOTE 12 on Android 13, Seeker on Android 15, two Wi-Fi Direct televisions in range; Bluetooth off; no prior group; location not granted):
Locally:
android-ci-harness: all tests pass, includingWifiDirectGroupFormationTest(18). The tests cover:sidrecord);bindings/kotlin: library tests and staging publish, the minified consumer app,check_android_aar.pyandcheck_android_test_results.py, all green. Lint's NewApi check passes: the API 29 calls sit behind in-function SDK checks rather than a suppression.react_native*guards pass.tsc: clean. JS harness: all files pass.Wi-Fi Direct comes back when Wi-Fi does (
76929a09)Found in the mixed-transport test.
discoverServicesfailed withNO_SERVICE_REQUESTS. An app started with Wi-Fi off never got Wi-Fi Direct at all.This part applies without
autoAccepttoo (the slot is reported up again). It could live in #505 if you prefer; say so and I'll move it.Risk
autoAccept.Not covered / known limits