fix(bindings): Android Wi-Fi Direct sends at once, rejoins its group, and starts from the config - #505
Merged
Conversation
… and starts from the config Four faults found smoke-testing Wi-Fi Direct on two Android phones (Infinix NOTE 12, Android 13; Solana Seeker, Android 15): - The send callback was registered at create only if a manager existed, and the manager is built later by enableTransport, so every body and acknowledgement waited for the 2s fallback poll (median ~3 s per delivery, 0.3 to 8 s). It is now registered for the slot and resolves the manager when it fires (median ~110 ms on the same phones). - WIFI_P2P_CONNECTION_CHANGED_ACTION is not sticky since Android 10, so a manager started inside an existing group never joined it. It now asks for the group once running. - start() auto-enabled internet, Nostr and Reticulum but not transports.wifiDirect, so the documented config left it off. - The module did not declare ACCESS_WIFI_STATE, CHANGE_WIFI_STATE or NEARBY_WIFI_DEVICES, so the transport reported itself unavailable on Android 13+. NEARBY_WIFI_DEVICES is declared neverForLocation and the 13+ check no longer demands a location grant. Guards: two source guards in the uniffi crate, a JS harness test, and NEARBY_WIFI_DEVICES in the AAR checker's required manifest entries.
This was referenced Oct 5, 2026
…nd Wi-Fi Direct switches by link From the v0.28 device test on two Android phones: - One Bluetooth departure arrived as two neighbor_lost (Android gives up on each stale address separately). ble_peer_lost now reports only a peer it knew. - A peer that left the Wi-Fi Direct group but stayed linked over Bluetooth LE, or the reverse, was reported lost and cleared from core discovery tracking. Both loss paths now skip the report while the other carrier still links to the peer. - transport_switched to WiFiDirect fired when the stream layer came up, which managers do at start with no peer; it now follows the first proved link and the last one lost. Four tests in the uniffi crate.
…irect loss is reported Found on the two phones after the previous commit: Bluetooth was switched off while both carriers linked the peer, then Wi-Fi Direct dropped. Android reports the adapter unavailable but delivers no per-link disconnect, so the peer still counted as linked over Bluetooth; the Wi-Fi Direct loss was skipped as "linked elsewhere" and the app listed a neighbour nothing reached. ble_status_changed(false) now drops the Bluetooth peers and reports each one lost once, unless Wi-Fi Direct still reaches it. A platform that reports its peers lost first (iOS does on radio loss) leaves none, and a late per-peer report adds nothing. Two tests in the uniffi crate; both fail without the change.
The previous commit moved transport_switched off the layer and onto the links, and decided the edge by reading the link count before and after each per-peer report. That count is gated on the layer being up. So it lies exactly when the layer goes down. iOS takes the layer down when the app backgrounds, and the OS kills the streams afterwards. With two links held, the flip emitted nothing, and then each late stream end saw zero links and emitted its own switch to None. Two switches away for one departure. Links proved while the layer was down did the mirror image: one switch to WiFiDirect per link. And the flip clears the links silently, so the core kept both neighbours until the late ends trickled in. The comment said a layer going down reports its links lost first. Android and Python do. iOS doesn't, and nothing enforced it. Let's stop relying on caller ordering. The edge is now decided in one place, from the visible count read under the same lock as the change, and the status call runs it too: a flip that hides N links switches once, a flip that reveals them switches once. The flip also reports every link it ended lost, unless Bluetooth still reaches the peer, the same shape ble_status_changed got. A peer disconnect now reports only a link the transport actually held, so the late ends, and the end of a stream that never proved anything, add nothing. While at it, the iOS BleManager comment that said the status call clears only the transport's peer map is no longer true; fix it.
The library now declares NEARBY_WIFI_DEVICES with neverForLocation, and on Android 13+ the Wi-Fi Direct manager checks that permission alone. That is only correct while the flag is there. The release check pinned the permission's name and nothing else, so an AAR that lost the flag passed, and every app on it would quietly need a location grant the manager never asks about. Discovery then fails with a bare ERROR instead of the transport saying it's unavailable. The check now refuses a declaration without the flag. The docs had it backwards too. The README still told apps to declare the Wi-Fi permissions themselves, without the flag, and the PR claimed an app's own flag-less declaration wins the merge. It doesn't. The merger combines non-conflicting attributes, so the library's flag lands in every app that adds it. That is what we want, but an app that really does derive location from Wi-Fi has to know to replace the declaration, and that it then needs fine location on 13+ that the SDK won't check for it. Say so. While at it, document that start() enables Wi-Fi Direct once, so the runtime grant has to come first. A refused enable is logged and never retried.
bahdotsh
approved these changes
Oct 6, 2026
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.
Four faults from the v0.28 device smoke test of Wi-Fi Direct on two Android phones: an Infinix NOTE 12 (Android 13) and a Solana Seeker (Android 15), with Bluetooth off and no internet. Main at c8a5d12 passes CI, but on the phones Wi-Fi Direct was off unless the app turned it on by hand, unavailable on Android 13+ unless the app declared permissions the guide doesn't list, about 3 s per delivery when it did run, and unreachable after any app restart.
What was wrong, and the fix
1. Every Wi-Fi Direct frame waited for the 2 s fallback poll.
wireTransportCallbacks()runs atcreateand registered the Wi-Fi Direct callback insidewifiDirectManager?.let. The manager is only ever built later, byenableTransport, so the callback was never registered in any app. Every body and every acknowledgement rodeWifiDirectManager's 2 s poll. Measured on the phones: median about 3 s per delivery (0.3 to 8 s), usually with a retry. A trace of one message showed it arriving in about 100 ms; the acknowledgement then sat in the queue until the next poll.Fix: register the callback whenever the config enables the slot, and resolve the manager when it fires, so a manager that
enableTransportreplaces needs no second registration. The field becomes@Volatile, since the core's thread reads it. iOS already registers when it builds its manager (OfflineProtocolModule.swift, thewifi_directarm ofenableTransport); Android now matches.2. After an app restart, the phone never rejoins its Wi-Fi Direct group. The group belongs to the system and outlives the process. Since Android 10,
WIFI_P2P_CONNECTION_CHANGED_ACTIONis no longer a sticky broadcast, so the receiver registered instartUnsafe()hears nothing about a group that already exists. The owner never listened and the client never dialled; the only recovery was removing the group in system settings. This reproduced after every restart, on both phones.Fix:
adoptExistingGroup()runs once when the manager is running:requestConnectionInfo, and ifgroupFormed, hand it tohandleConnectionChanged(true), the same path the broadcast takes.3.
transports.wifiDirect.enabled: truedid nothing.start()auto-enables internet, Nostr and Reticulum from their config sections but not this one, and nothing is logged. The demo app sat on "Searching for peers" next to a live group.Fix:
start()enableswifiDirectafter the native start, logging rather than throwing on failure, like the others. iOS behaviour change: an iOS app withwifiDirect.enabled: truenow starts the Network framework peer stream atstart(), and gets the Local Network prompt on first run. This is noted in the changelog.4. Missing Wi-Fi Direct permissions. The module declared the Bluetooth permissions but not
ACCESS_WIFI_STATE,CHANGE_WIFI_STATEorNEARBY_WIFI_DEVICES. An app following the guide got "WiFi P2P is not available on this device" on Android 13+.Fix: the manifest declares them, with
NEARBY_WIFI_DEVICESset toneverForLocation(the manager never derives location), plusINTERNETfor the stream socket. On 13+hasRequiredPermissions()no longer demandsACCESS_FINE_LOCATION. That is whatneverForLocationallows, and it is verified below with location revoked. Android 12 and lower still need fine location for peer discovery, which stays with the app (as BLE's location does on 11 and lower), and the guide now says so.Validation
On the phones (demo app built from this branch, location revoked on both, no app-side workaround):
Wi-Fi Direct transport auto-enabled.Joining a group formed before startinto a group left over from the previous run. The client, started a minute before the owner, redialled and proved its stream 16 s after the owner came up.Locally:
cargo test -p offline-protocol-uniffi --lib: 191 passed. The two new guards fail on main's Kotlin and pass here.cargo fmt --all -- --check: clean.npm run test:js: all 12 harness files pass, including the newwifi-direct-config.test.js, which fails 3 of 4 against main'sindex.ts.npx tsc --noEmit: clean.android-ci-harnesswith Gradle 8.9 and JDK 17: 571 tests, 0 failures.scripts/tests/test_check_android_aar.py: 43 passed.Guards added
react_native_android_wifi_direct_callback_does_not_wait_for_a_manager: pins the slot-gated registration and the volatile field, and refuses thewifiDirectManager?.letform.react_native_android_wifi_direct_joins_a_group_formed_before_start: pins the call instartUnsafe()and that the adoption goes throughhandleConnectionChanged(true).js-ci-harness/wifi-direct-config.test.js: enables with the whole section, after the native start; an absent or disabled section starts nothing; a failure warns without failingstart().NEARBY_WIFI_DEVICESis added toREQUIRED_IN_MANIFESTincheck_android_aar.py.Risk
requestConnectionInfoat start, and manifest permissions that merge into the app. The merger combines non-conflicting attributes, soneverForLocationreaches every consuming app's merged manifest, including one that declaredNEARBY_WIFI_DEVICESitself without the flag. An app that derives location from Wi-Fi must replace the declaration withtools:node="replace"and then also holdACCESS_FINE_LOCATIONon 13+ (see the README).Added after review: neighbour and transport events (
46203fcc,be01882a)The extended device matrix turned up three event bugs that every app sees:
neighbor_lost. Android gives up on each stale address of a peer separately.ble_peer_lostnow reports only a peer it knew.neighbor_lostand cleared core discovery tracking. Both loss paths now skip while another carrier links the peer.transport_switchedto WiFiDirect fired when the stream layer started, peer or no peer. It now follows the first proved link and the last one lost.ble_status_changed(false)now ends the core's Bluetooth peers, reporting each lost once unless Wi-Fi Direct still reaches it. The Android side of the same outage, dead GATT links left in place, is fix(bindings): Bluetooth peers find each other in busy rooms, and Android survives Bluetooth toggles and stack crashes #511.Six uniffi tests cover these. The two for the last fix fail without it.
796db2a7correctsdocs/react-native-integration.md:start()now restores Wi-Fi Direct from the config.Not in this PR