[{"data":1,"prerenderedAt":1823},["ShallowReactive",2],{"i-ph:moon-bold":3,"i-ph:globe-simple":8,"i-ph:caret-down":10,"i-ph:list":12,"i-ph:heart-fill":14,"i-ph:discord-logo-bold":16,"i-ph:github-logo-bold":18,"i-ph:cookie-bold":20,"blog-article-en-wifi-aware-transport":22,"i-ph:arrow-left":1821},{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":7},0,256,false,"\u003Cpath fill=\"currentColor\" d=\"M236.37 139.4a12 12 0 0 0-12-3A84.07 84.07 0 0 1 119.6 31.59a12 12 0 0 0-15-15a108.86 108.86 0 0 0-54.91 38.48A108 108 0 0 0 136 228a107.1 107.1 0 0 0 64.93-21.69a108.86 108.86 0 0 0 38.44-54.94a12 12 0 0 0-3-11.97m-49.88 47.74A84 84 0 0 1 68.86 69.51a84.9 84.9 0 0 1 23.41-21.22Q92 52.13 92 56a108.12 108.12 0 0 0 108 108q3.87 0 7.71-.27a84.8 84.8 0 0 1-21.22 23.41\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":9},"\u003Cpath fill=\"currentColor\" d=\"M128 24a104 104 0 1 0 104 104A104.12 104.12 0 0 0 128 24m87.62 96h-39.83c-1.79-36.51-15.85-62.33-27.38-77.6a88.19 88.19 0 0 1 67.22 77.6ZM96.23 136h63.54c-2.31 41.61-22.23 67.11-31.77 77c-9.55-9.9-29.46-35.4-31.77-77m0-16c2.31-41.61 22.23-67.11 31.77-77c9.55 9.93 29.46 35.43 31.77 77Zm11.36-77.6C96.06 57.67 82 83.49 80.21 120H40.37a88.19 88.19 0 0 1 67.22-77.6M40.37 136h39.84c1.82 36.51 15.85 62.33 27.38 77.6A88.19 88.19 0 0 1 40.37 136m108 77.6c11.53-15.27 25.56-41.09 27.38-77.6h39.84a88.19 88.19 0 0 1-67.18 77.6Z\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":11},"\u003Cpath fill=\"currentColor\" d=\"m213.66 101.66l-80 80a8 8 0 0 1-11.32 0l-80-80a8 8 0 0 1 11.32-11.32L128 164.69l74.34-74.35a8 8 0 0 1 11.32 11.32\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":13},"\u003Cpath fill=\"currentColor\" d=\"M224 128a8 8 0 0 1-8 8H40a8 8 0 0 1 0-16h176a8 8 0 0 1 8 8M40 72h176a8 8 0 0 0 0-16H40a8 8 0 0 0 0 16m176 112H40a8 8 0 0 0 0 16h176a8 8 0 0 0 0-16\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":15},"\u003Cpath fill=\"currentColor\" d=\"M240 102c0 70-103.79 126.66-108.21 129a8 8 0 0 1-7.58 0C119.79 228.66 16 172 16 102a62.07 62.07 0 0 1 62-62c20.65 0 38.73 8.88 50 23.89C139.27 48.88 157.35 40 178 40a62.07 62.07 0 0 1 62 62\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":17},"\u003Cpath fill=\"currentColor\" d=\"M108 136a16 16 0 1 1-16-16a16 16 0 0 1 16 16m56-16a16 16 0 1 0 16 16a16 16 0 0 0-16-16m76.07 76.56l-67 29.71A20.15 20.15 0 0 1 146 214.9l-8.54-23.13c-3.13.14-6.27.24-9.45.24s-6.32-.1-9.45-.24L110 214.9a20.19 20.19 0 0 1-27.08 11.37l-67-29.71a19.93 19.93 0 0 1-11.3-23.15L34.15 57a20 20 0 0 1 16.22-14.81l36.06-5.93a20.26 20.26 0 0 1 22.79 14.84l4.41 17.41c4.74-.33 9.52-.51 14.37-.51s9.63.18 14.37.51l4.41-17.41a20.25 20.25 0 0 1 22.79-14.84l36.06 5.93A20 20 0 0 1 221.85 57l29.53 116.38a19.93 19.93 0 0 1-11.31 23.18M227.28 176L199.23 65.46l-30.07-4.94l-2.84 11.17c2.9.58 5.78 1.2 8.61 1.92a12 12 0 1 1-5.86 23.27A168.4 168.4 0 0 0 128 92a168.4 168.4 0 0 0-41.07 4.88a12 12 0 0 1-5.86-23.27c2.83-.72 5.71-1.34 8.61-1.92l-2.83-11.17l-30.08 4.94L28.72 176l60.22 26.7l5-13.57c-4.37-.76-8.67-1.65-12.88-2.71a12 12 0 0 1 5.86-23.28A168.4 168.4 0 0 0 128 168a168.4 168.4 0 0 0 41.07-4.88a12 12 0 0 1 5.86 23.28c-4.21 1.06-8.51 1.95-12.88 2.71l5 13.57Z\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":19},"\u003Cpath fill=\"currentColor\" d=\"M212.62 75.17A63.7 63.7 0 0 0 206.39 26A12 12 0 0 0 196 20a63.71 63.71 0 0 0-50 24h-20a63.71 63.71 0 0 0-50-24a12 12 0 0 0-10.39 6a63.7 63.7 0 0 0-6.23 49.17A61.5 61.5 0 0 0 52 104v8a60.1 60.1 0 0 0 45.76 58.28A43.66 43.66 0 0 0 92 192v4H76a20 20 0 0 1-20-20a44.05 44.05 0 0 0-44-44a12 12 0 0 0 0 24a20 20 0 0 1 20 20a44.05 44.05 0 0 0 44 44h16v12a12 12 0 0 0 24 0v-40a20 20 0 0 1 40 0v40a12 12 0 0 0 24 0v-40a43.66 43.66 0 0 0-5.76-21.72A60.1 60.1 0 0 0 220 112v-8a61.5 61.5 0 0 0-7.38-28.83M196 112a36 36 0 0 1-36 36h-48a36 36 0 0 1-36-36v-8a37.87 37.87 0 0 1 6.13-20.12a11.65 11.65 0 0 0 1.58-11.49a39.9 39.9 0 0 1-.4-27.72a39.87 39.87 0 0 1 26.41 17.8a12 12 0 0 0 10.1 5.53h32.35a12 12 0 0 0 10.11-5.53a39.84 39.84 0 0 1 26.41-17.8a39.9 39.9 0 0 1-.4 27.72a12 12 0 0 0 1.61 11.53A37.85 37.85 0 0 1 196 104Z\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":21},"\u003Cpath fill=\"currentColor\" d=\"M167.31 160.69a16 16 0 1 1-22.62 0a16 16 0 0 1 22.62 0m-86.62-8a16 16 0 1 0 22.62 0a16 16 0 0 0-22.62 0m14.62-33.38a16 16 0 1 0-22.62 0a16 16 0 0 0 22.62 0m48-6.62a16 16 0 1 0 0 22.62a16 16 0 0 0 0-22.62M236 128A108 108 0 1 1 128 20a12 12 0 0 1 12 12a36 36 0 0 0 36 36a12 12 0 0 1 12 12a36 36 0 0 0 36 36a12 12 0 0 1 12 12m-24.67 10.65A60.17 60.17 0 0 1 165 91a60.17 60.17 0 0 1-47.66-46.32a84 84 0 1 0 94 94Z\"\u002F>",{"id":23,"title":24,"body":25,"category":1808,"date":1809,"description":1810,"extension":1811,"meta":1812,"navigation":1813,"path":1814,"readingTime":1815,"seo":1816,"seoDescription":1817,"seoTitle":1818,"stem":1819,"__hash__":1820},"blog_en\u002Fblog\u002Fwifi-aware-transport.md","Wi-Fi Aware Transport Design — Neighbor Discovery & Data Paths",{"type":26,"value":27,"toc":1747},"minimark",[28,42,61,66,164,167,174,188,195,200,207,238,253,256,271,277,281,288,295,298,325,331,335,353,360,364,390,393,404,411,417,421,432,435,461,464,479,493,499,503,520,537,569,573,583,609,615,618,623,629,636,659,670,673,684,690,694,707,714,752,755,759,775,780,799,814,820,824,842,850,870,873,876,890,896,900,907,911,930,933,936,942,946,996,999,1006,1012,1016,1031,1057,1068,1072,1097,1100,1107,1124,1139,1145,1149,1152,1187,1194,1201,1218,1245,1249,1255,1258,1271,1277,1283,1289,1300,1303,1331,1345,1348,1703,1706,1712,1716],[29,30,31,32,36,37,41],"p",{},"The article covers the Android-only Aware session lifecycle, the publish \u002F\nsubscribe discovery model, the ",[33,34,35],"strong",{},"two-phase role-split handshake"," that\nsynchronizes ",[38,39,40],"code",{},"requestNetwork"," on both sides within the framework's ~500 ms\nwindow, the per-peer link pool with idle sweeping, the IPv6 + custom-DNS\ntrick that lets a single OkHttp client serve both LAN and Aware, and the\nprewarmer that triggers peer Aware startup via BLE.",[29,43,44,45,50,51,55,56,60],{},"For the broader fallback chain, see ",[46,47,49],"a",{"href":48},"\u002Fblog\u002Fchat-architecture","Chat Architecture",".\nFor the BLE transport that takes over when Aware is unavailable, see\n",[46,52,54],{"href":53},"\u002Fblog\u002Fble-transport","BLE Transport",". For how the shared ChaCha20 key reused\nas the Aware PMK is established, see ",[46,57,59],{"href":58},"\u002Fblog\u002Fpairing-flow","Pairing Flow",".",[62,63,65],"h2",{"id":64},"table-of-contents","Table of Contents",[67,68,69,76,82,88,94,100,106,112,122,128,134,140,146,152,158],"ul",{},[70,71,72],"li",{},[46,73,75],{"href":74},"#why-wi-fi-aware","Why Wi-Fi Aware?",[70,77,78],{},[46,79,81],{"href":80},"#where-aware-sits-in-the-fallback-chain","Where Aware Sits in the Fallback Chain",[70,83,84],{},[46,85,87],{"href":86},"#session-lifecycle-attach--publish--subscribe","Session Lifecycle: Attach → Publish + Subscribe",[70,89,90],{},[46,91,93],{"href":92},"#discovery--role-assignment","Discovery & Role Assignment",[70,95,96],{},[46,97,99],{"href":98},"#the-two-phase-handshake-hello--ready","The Two-Phase Handshake (hello + ready)",[70,101,102],{},[46,103,105],{"href":104},"#ndp-requestnetwork--the-500-ms-window","NDP requestNetwork — The 500 ms Window",[70,107,108],{},[46,109,111],{"href":110},"#per-peer-link-pool--idle-sweeping","Per-Peer Link Pool & Idle Sweeping",[70,113,114],{},[46,115,117,118,121],{"href":116},"#ipv6-addressing--the-plain-aware-peer-dns-trick","IPv6 Addressing & the ",[38,119,120],{},"plain-aware-peer"," DNS Trick",[70,123,124],{},[46,125,127],{"href":126},"#cryptography-pmk-derivation--chacha20-reuse","Cryptography: PMK Derivation & ChaCha20 Reuse",[70,129,130],{},[46,131,133],{"href":132},"#message-send-path-end-to-end","Message Send Path (End-to-End)",[70,135,136],{},[46,137,139],{"href":138},"#file-download-path-end-to-end","File Download Path (End-to-End)",[70,141,142],{},[46,143,145],{"href":144},"#prewarming-ble-triggered-aware-startup","Prewarming: BLE-Triggered Aware Startup",[70,147,148],{},[46,149,151],{"href":150},"#failure-modes--the-fast-skip-flag","Failure Modes & the Fast-Skip Flag",[70,153,154],{},[46,155,157],{"href":156},"#key-constants-reference","Key Constants Reference",[70,159,160],{},[46,161,163],{"href":162},"#design-trade-offs-recap","Design Trade-offs Recap",[62,165,75],{"id":166},"why-wi-fi-aware",[29,168,169,170,173],{},"Wi-Fi Aware (IEEE 802.11bc, formerly NAN — Neighbor Awareness Networking)\nis a Wi-Fi Alliance certification that lets two devices ",[33,171,172],{},"discover each\nother and exchange data without any Wi-Fi infrastructure"," — no AP, no\nrouter, no DHCP. PlainApp uses it for two scenarios that LAN cannot cover:",[67,175,176,182],{},[70,177,178,181],{},[33,179,180],{},"Different SSIDs \u002F VLANs."," A phone on the guest network and a laptop on\nthe IoT VLAN are both \"online\" via Wi-Fi but cannot reach each other's\nIP. Aware creates a direct device-to-device data path that bypasses the\ninfrastructure entirely.",[70,183,184,187],{},[33,185,186],{},"No infrastructure at all."," Two devices in the wilderness with Wi-Fi\non but no AP can still chat. (BLE also covers this, but Aware is much\nfaster — ~10 ms round trips vs seconds, and MB\u002Fs vs tens of KB\u002Fs.)",[29,189,190],{},[191,192],"img",{"alt":193,"src":194},"Diagram 1","\u002Fblog\u002Fwifi-aware-transport\u002Fdiagram-01.svg",[196,197,199],"h3",{"id":198},"platform-constraints","Platform constraints",[29,201,202,203,206],{},"Wi-Fi Aware is ",[33,204,205],{},"Android-only"," in PlainApp:",[67,208,209,227],{},[70,210,211,212,215,216,219,220,223,224,60],{},"Android 13 (API 33) is the minimum — the ",[38,213,214],{},"WifiAwareNetworkSpecifier.Builder","\nwith ",[38,217,218],{},"setPort()"," and ",[38,221,222],{},"setPmk()"," overloads that PlainApp depends on require\n",[38,225,226],{},"isTPlus()",[70,228,229,230,233,234,237],{},"iOS does not expose Wi-Fi Aware to third-party apps. iOS PlainApp falls\ndirectly from LAN to BLE; the ",[38,231,232],{},"WifiAwareTransport"," object is not even\ncompiled into the iOS target (",[38,235,236],{},"@RequiresApi(Build.VERSION_CODES.S)"," +\nandroidMain source set).",[29,239,240,241,244,245,248,249,252],{},"This is why the ",[38,242,243],{},"PeerTransportRouter.buildList"," calls\n",[38,246,247],{},"createWifiAwareTransport()"," — a factory that returns ",[38,250,251],{},"null"," on iOS.",[62,254,81],{"id":255},"where-aware-sits-in-the-fallback-chain",[29,257,258,259,262,263,266,267,270],{},"PlainApp's ",[38,260,261],{},"PeerTransportRouter"," is an ordered list. For each ",[38,264,265],{},"send"," or\n",[38,268,269],{},"downloadFile"," call, it walks the list and tries each transport until one\nsucceeds; failures cascade down.",[29,272,273],{},[191,274],{"alt":275,"src":276},"Diagram 2","\u002Fblog\u002Fwifi-aware-transport\u002Fdiagram-02.svg",[196,278,280],{"id":279},"why-is-aware-the-middle-and-not-the-first","Why is Aware \"the middle\" and not \"the first\"?",[29,282,283,284,287],{},"Because ",[33,285,286],{},"LAN is almost always faster when available",". A same-subnet Wi-Fi\nhop through an AP is a single 802.11 frame exchange; an Aware data path\nadds an NDP setup (~5 s on first use) plus a second Wi-Fi radio context\nfor the device-to-device link. If both are reachable, LAN wins on latency\nand throughput.",[29,289,290,291,294],{},"Conversely, BLE is ",[33,292,293],{},"always slower"," — but it works whenever both devices\nare paired. Aware is in the middle: faster than BLE, slower than LAN, and\nonly available on Android 13+ devices with Wi-Fi on.",[62,296,87],{"id":297},"session-lifecycle-attach-publish-subscribe",[29,299,300,301,304,305,308,309,312,313,316,317,320,321,324],{},"A Wi-Fi Aware session is ",[33,302,303],{},"process-wide",". There is exactly one\n",[38,306,307],{},"WifiAwareSession"," per device; within it, PlainApp runs ",[33,310,311],{},"one publish\nsession"," (so peers can discover us) and ",[33,314,315],{},"one subscribe session"," (so we\ncan discover peers). Both are started the moment ",[38,318,319],{},"AwareSession.start()","\ncompletes the ",[38,322,323],{},"attach"," callback.",[29,326,327],{},[191,328],{"alt":329,"src":330},"Diagram 3","\u002Fblog\u002Fwifi-aware-transport\u002Fdiagram-03.svg",[196,332,334],{"id":333},"why-publish-and-subscribe-on-the-same-device","Why publish AND subscribe on the same device?",[29,336,337,338,341,342,345,346,349,350,352],{},"The Wi-Fi Aware discovery model is ",[33,339,340],{},"asymmetric",": a publisher advertises\na service, a subscriber scans for it. To make discovery symmetric (both\ndevices discover each other), PlainApp does ",[33,343,344],{},"both at once",". Without this,\ndevice A would have to know in advance whether it's the publisher or the\nsubscriber for a given peer — but peer roles are determined later by\n",[38,347,348],{},"clientId"," comparison (see ",[46,351,93],{"href":92},").",[29,354,355,356,359],{},"Publishing and subscribing simultaneously means each device sees the\nother's ",[38,357,358],{},"onServiceDiscovered"," (as subscriber) AND receives the other's\nhello messages (as publisher) — both directions of the handshake are\nalways available.",[196,361,363],{"id":362},"auto-restart-on-termination","Auto-restart on termination",[29,365,366,367,370,371,374,375,378,379,381,382,385,386,389],{},"Some Android variants (MIUI in particular) kill long-running Aware\nsessions to save battery. PlainApp handles this in the ",[38,368,369],{},"onSessionTerminated","\ncallbacks: it nulls out the terminated session and immediately calls\n",[38,372,373],{},"publishOwnService"," \u002F ",[38,376,377],{},"subscribeOwnService"," again on the still-attached\n",[38,380,307],{},". The attach session itself is not lost — only the\npublish\u002Fsubscribe discovery session. Peer handles from before the\ntermination become stale, which is why ",[38,383,384],{},"awaitPeerHandle"," checks the\n",[38,387,388],{},"discoveredAt"," timestamp and discards handles older than 30 s.",[62,391,93],{"id":392},"discovery-role-assignment",[29,394,395,396,399,400,403],{},"The Wi-Fi Aware data-path protocol requires ",[33,397,398],{},"one side to act as the\npublisher (server)"," and the other as the ",[33,401,402],{},"subscriber (client)",". Both\nsides cannot simultaneously be the initiator — the framework rejects\nrequests without a matching counterpart.",[29,405,406,407,410],{},"PlainApp assigns roles ",[33,408,409],{},"deterministically per peer"," using a simple\nlexicographic comparison of clientIds:",[29,412,413],{},[191,414],{"alt":415,"src":416},"Diagram 4","\u002Fblog\u002Fwifi-aware-transport\u002Fdiagram-04.svg",[196,418,420],{"id":419},"why-deterministic-and-not-negotiated","Why deterministic and not negotiated?",[29,422,423,424,427,428,431],{},"A negotiated approach (e.g. \"lower MAC is the server\") would require an\nextra message exchange. The lexicographic comparison is ",[33,425,426],{},"idempotent,\nsymmetric, and stateless",": both devices compute the same role for the\nsame pair without any communication. The clientId is a 13-character short\nUUID, so ties (",[38,429,430],{},"clientId == peer.id",") only happen when comparing a peer to\nitself — which never reaches the transport.",[29,433,434],{},"The role determines two things downstream:",[436,437,438,447],"ol",{},[70,439,440,443,444,446],{},[33,441,442],{},"Who drives the retry loop."," Only the client retries\n",[38,445,40],{},"; the server makes exactly one attempt per hello\nreceived. This is critical for the 500 ms window (next section).",[70,448,449,452,453,456,457,460],{},[33,450,451],{},"Who sets the port."," The publisher calls ",[38,454,455],{},"setPort(httpsPort)"," because\nit's the one accepting incoming connections on its HTTPS server port.\nThe subscriber doesn't set a port — it learns the peer's port from the\n",[38,458,459],{},"WifiAwareNetworkInfo"," after the data path is established.",[62,462,99],{"id":463},"the-two-phase-handshake-hello-ready",[29,465,466,467,470,471,474,475,478],{},"The hardest part of Wi-Fi Aware data-path setup is ",[33,468,469],{},"timing",". The\nframework requires both sides to call ",[38,472,473],{},"connectivityManager.requestNetwork","\nwithin roughly 500 ms of each other — if one side calls it before the\nother has registered its matching request, the framework rejects it\nimmediately with ",[38,476,477],{},"onUnavailable"," (\"releaseRequestAsUnfulfillableByAnyFactory\").",[29,480,481,482,485,486,489,490,492],{},"PlainApp solves this with a ",[33,483,484],{},"two-message application-layer handshake","\nthat runs on top of the Aware L2 message channel (the same ",[38,487,488],{},"sendMessage","\nAPI used by ",[38,491,358],{},"):",[29,494,495],{},[191,496],{"alt":497,"src":498},"Diagram 5","\u002Fblog\u002Fwifi-aware-transport\u002Fdiagram-05.svg",[196,500,502],{"id":501},"why-two-messages-hello-ready-instead-of-just-one","Why two messages (hello + ready) instead of just one?",[29,504,505,506,509,510,512,513,515,516,519],{},"The hello alone is not enough because of ",[33,507,508],{},"direction asymmetry",". The\nsubscriber can send hello the instant it discovers the publisher (in\n",[38,511,358],{},"), but the publisher cannot start\n",[38,514,40],{}," until it has the subscriber's ",[38,517,518],{},"PeerHandle",", which it\nonly learns by receiving the hello. So the hello serves two purposes:",[436,521,522,531],{},[70,523,524,527,528,60],{},[33,525,526],{},"Deliver the subscriber's PeerHandle to the publisher."," The publisher\nneeds it to build the ",[38,529,530],{},"WifiAwareNetworkSpecifier",[70,532,533,536],{},[33,534,535],{},"Signal intent to connect."," Receiving the hello tells the publisher\n\"the subscriber is about to requestNetwork, so I should too.\"",[29,538,539,540,543,544,547,548,550,551,553,554,557,558,560,561,564,565,568],{},"The ",[38,541,542],{},"ready"," receipt exists for the ",[33,545,546],{},"opposite direction"," — to tell the\nsubscriber \"the publisher has registered its requestNetwork.\" Without it,\nthe subscriber's ",[38,549,40],{}," might race ahead of the publisher's\nand get rejected by the framework. The ",[38,552,542],{}," receipt is a ",[33,555,556],{},"non-blocking\nsignal",": the subscriber doesn't wait for it before calling\n",[38,559,40],{}," (that would add a round trip), but if it arrives while\nthe subscriber is in ",[38,562,563],{},"IDLE"," state (between retry attempts), the subscriber\ncan immediately retry without waiting for the ",[38,566,567],{},"RETRY_DELAY_MS"," gap.",[196,570,572],{"id":571},"the-retry-loop-asymmetry","The retry-loop asymmetry",[29,574,575,576,579,580,582],{},"This is the most subtle part of the design. ",[33,577,578],{},"Only the subscriber\nretries."," The publisher makes exactly one ",[38,581,40],{}," attempt per\nhello received. This is because:",[67,584,585,595],{},[70,586,587,588,591,592,594],{},"If both sides retried independently, their retry cycles would drift\nout of phase (different ",[38,589,590],{},"delay()"," durations, different GC pauses), and\nthe two ",[38,593,40],{}," calls would rarely overlap inside the 500 ms\nwindow.",[70,596,597,598,601,602,605,606,608],{},"The subscriber's retry loop sends a fresh hello on each attempt, which\nre-triggers the publisher's ",[38,599,600],{},"buildLink"," via ",[38,603,604],{},"publishHelloListeners",".\nThis guarantees the publisher's ",[38,607,40],{}," always follows the\nhello by ~50 ms, well inside the 500 ms window.",[29,610,611,612,60],{},"This is documented in detail in\n",[38,613,614],{},"AwarePeerLink.build",[62,616,105],{"id":617},"ndp-requestnetwork-the-500-ms-window",[29,619,539,620,622],{},[38,621,40],{}," call is the most timing-sensitive operation in the\nAware transport. Here's what happens on each side:",[29,624,625],{},[191,626],{"alt":627,"src":628},"Diagram 6","\u002Fblog\u002Fwifi-aware-transport\u002Fdiagram-06.svg",[196,630,632,633,635],{"id":631},"what-the-onunavailable-callback-means","What the ",[38,634,477],{}," callback means",[29,637,638,640,641,643,644,647,648,651,652,655,656,658],{},[38,639,477],{}," fires when the framework rejects the ",[38,642,40],{},"\nbefore finding a matching peer request. ",[33,645,646],{},"The PeerHandle itself is still\nvalid"," — only the NDP (Neighbor Discovery Protocol) pairing failed\nbecause the other side hadn't registered yet. PlainApp deliberately does\n",[33,649,650],{},"not"," call ",[38,653,654],{},"session.invalidatePeerHandle"," in this case, because\ninvalidating the handle would discard the only signal that\n",[38,657,358],{}," was ever called (it fires once per peer per\nsubscribe session lifetime). With the handle preserved, the retry can\nreuse it instead of waiting for a fresh discovery.",[29,660,661,662,665,666,669],{},"The same applies to the publisher-side handle from ",[38,663,664],{},"onMessageReceived"," —\nthe publisher keeps the ",[38,667,668],{},"publishPeerHandles[fromCid]"," entry across failed\nattempts, so the subscriber's next hello re-uses the cached handle instead\nof being dropped.",[62,671,111],{"id":672},"per-peer-link-pool-idle-sweeping",[29,674,675,676,679,680,683],{},"Each paired peer gets its own ",[38,677,678],{},"AwarePeerLink"," object, owned by the\nprocess-wide ",[38,681,682],{},"AwareLinkPool",". The pool handles discovery events, link\nreuse, and idle eviction.",[29,685,686],{},[191,687],{"alt":688,"src":689},"Diagram 7","\u002Fblog\u002Fwifi-aware-transport\u002Fdiagram-07.svg",[196,691,693],{"id":692},"why-no-auto-build-on-discovery","Why no auto-build on discovery?",[29,695,696,697,700,701,703,704,706],{},"The pool explicitly ",[33,698,699],{},"does not"," build a link when\n",[38,702,358],{}," fires. This is a critical decision: a busy coffee\nshop might have 100 PlainApp devices all publishing the \"plain-peer\"\nservice. If each discovery triggered a ",[38,705,40],{},", the framework\nwould be flooded with NDP setup attempts and the Wi-Fi radio would be\nsaturated.",[29,708,709,710,713],{},"Instead, the pool ",[33,711,712],{},"only records the PeerHandle"," and waits for one of:",[436,715,716,730,742],{},[70,717,718,721,722,725,726,729],{},[33,719,720],{},"The local user sends a message"," → ",[38,723,724],{},"WifiAwareTransport.send"," →\n",[38,727,728],{},"pool.buildLink(peer)"," (sender-side trigger).",[70,731,732,721,735,725,738,741],{},[33,733,734],{},"The remote peer sends a hello",[38,736,737],{},"onPublishHelloReceived",[38,739,740],{},"buildLink(peer)"," (receiver-side trigger).",[70,743,744,721,747,725,750,741],{},[33,745,746],{},"The remote peer sends a ready",[38,748,749],{},"onSubscribeReadyReceived",[38,751,740],{},[29,753,754],{},"This way, links are only built for peers that the user is actually\nexchanging messages with — not every PlainApp device in radio range.",[196,756,758],{"id":757},"idle-sweep","Idle sweep",[29,760,761,762,765,766,219,768,770,771,774],{},"Every 10 seconds, the pool walks all links and closes any whose\n",[38,763,764],{},"lastActiveAt"," is older than 60 seconds. Each ",[38,767,265],{},[38,769,269],{},"\ncalls ",[38,772,773],{},"link.touch()"," to refresh the timestamp. This reclaims the Wi-Fi\nradio context and OkHttp connection pool for peers the user has stopped\nchatting with — important because Android limits the number of\nsimultaneous Aware data paths to roughly 4–10 (device-dependent).",[62,776,117,778,121],{"id":777},"ipv6-addressing-the-plain-aware-peer-dns-trick",[38,779,120],{},[29,781,782,783,786,787,790,791,794,795,798],{},"Wi-Fi Aware data paths use ",[33,784,785],{},"link-local IPv6 only",". There is no IPv4, no\nDNS server, no DHCP. The peer's IPv6 address is delivered via the\n",[38,788,789],{},"WifiAwareNetworkInfo.peerIpv6Addr"," field in\n",[38,792,793],{},"onCapabilitiesChanged"," — a ",[38,796,797],{},"fe80::..."," address that's only meaningful\non the Aware network interface.",[29,800,801,802,805,806,809,810,813],{},"PlainApp needs to send HTTPS requests to this address, but OkHttp's\n",[38,803,804],{},"https:\u002F\u002F"," URL parsing refuses raw IPv6 literals in a hostname\n(",[38,807,808],{},"https:\u002F\u002F[fe80::abcd]:8443\u002F"," works, but routing it through a custom\n",[38,811,812],{},"Dns"," resolver is cleaner). The trick:",[29,815,816],{},[191,817],{"alt":818,"src":819},"Diagram 8","\u002Fblog\u002Fwifi-aware-transport\u002Fdiagram-08.svg",[196,821,823],{"id":822},"why-a-sentinel-hostname","Why a sentinel hostname?",[29,825,826,827,830,831,833,834,837,838,841],{},"The alternative — passing the IPv6 literal directly in the URL — would\nrequire every call site to know about the link-local address. By using a\nsentinel hostname, the URL construction is identical for LAN and Aware:\nboth produce a valid ",[38,828,829],{},"https:\u002F\u002F\u003Chost>:\u003Cport>\u002Fpeer_graphql"," URL that OkHttp\ncan parse. The only difference is the ",[38,832,812],{}," implementation bound to the\nclient — LAN uses the system DNS, Aware uses ",[38,835,836],{},"awareDns(peerIpv6)"," which\nreturns the cached link-local address for the sentinel hostname and\nfalls through to ",[38,839,840],{},"Dns.SYSTEM"," for anything else.",[196,843,845,846,849],{"id":844},"why-networksocketfactory","Why ",[38,847,848],{},"network.socketFactory","?",[29,851,852,853,856,857,859,860,863,864,866,867,60],{},"Android's ",[38,854,855],{},"Network"," object represents a specific network interface (in\nthis case, the Aware data path). By calling ",[38,858,848],{}," and\npassing it to OkHttp's ",[38,861,862],{},"socketFactory"," config, we force all TCP sockets\nto be created on the Aware interface — ",[33,865,650],{}," the default Wi-Fi or\ncellular interface. Without this, the OS would route the request via the\ndefault network, where the link-local IPv6 is unreachable, and the\nrequest would fail with ",[38,868,869],{},"ENETUNREACH",[62,871,127],{"id":872},"cryptography-pmk-derivation-chacha20-reuse",[29,874,875],{},"Wi-Fi Aware supports an optional PMK (Pairwise Master Key) for the data\npath. When set, the L2 link itself is encrypted with that PMK — the\nWi-Fi radio handles encryption, no application-layer crypto needed.",[29,877,878,879,882,883,219,886,889],{},"PlainApp derives the PMK from the ",[33,880,881],{},"same ChaCha20 shared key"," that\n",[38,884,885],{},"LanTransport",[38,887,888],{},"BleTransport"," use for application-layer encryption:",[29,891,892],{},[191,893],{"alt":894,"src":895},"Diagram 9","\u002Fblog\u002Fwifi-aware-transport\u002Fdiagram-09.svg",[196,897,899],{"id":898},"why-truncate-to-32-bytes","Why truncate to 32 bytes?",[29,901,902,903,906],{},"The Wi-Fi Aware PMK must be exactly 32 bytes (256 bits). The ChaCha20\nshared key from pairing is also 32 bytes in the normal case, so the\n",[38,904,905],{},"raw.size == 32"," branch is the common path. The truncation\u002Fpadding\nfallback handles the (theoretical) case where the key was stored shorter\n— padding with zeros to 32 bytes is a defensive measure, not something\nthat happens in practice with properly paired peers.",[196,908,910],{"id":909},"the-signed-envelope-is-identical-to-lan","The signed envelope is identical to LAN",[29,912,283,913,916,917,919,920,923,924,927,928,60],{},[38,914,915],{},"createCryptoHttpClient"," is the same factory used by\n",[38,918,885],{},", the L7 crypto on Aware is ",[33,921,922],{},"byte-for-byte identical"," to\nLAN. The server-side ",[38,925,926],{},"PeerGraphQLService"," doesn't know (or care) which\ntransport delivered the request — it just sees a signed, encrypted\nGraphQL payload and decrypts it with the peer's shared key. This is the\n\"one codebase, many transports\" principle documented in\n",[46,929,49],{"href":48},[62,931,133],{"id":932},"message-send-path-end-to-end",[29,934,935],{},"Putting it all together — what happens when a chat message is sent over\nWi-Fi Aware:",[29,937,938],{},[191,939],{"alt":940,"src":941},"Diagram 10","\u002Fblog\u002Fwifi-aware-transport\u002Fdiagram-10.svg",[196,943,945],{"id":944},"notable-design-choices","Notable design choices",[67,947,948,964,973],{},[70,949,950,953,954,956,957,959,960,963],{},[33,951,952],{},"Connection reuse."," Unlike ",[38,955,888],{},", which tears down the GATT\nconnection after every request, ",[38,958,232],{}," ",[33,961,962],{},"reuses the Aware\ndata path"," for as many requests as the user makes within the 60 s idle\nwindow. The first request pays the ~400 ms handshake; subsequent\nrequests are ~10 ms round trips.",[70,965,966,969,970,972],{},[33,967,968],{},"Same crypto as LAN."," The ChaCha20 interceptor and signed envelope are\nbyte-identical to LAN. The peer's ",[38,971,926],{}," doesn't know\nwhich transport delivered the request.",[70,974,975,978,979,981,982,985,986,988,989,991,992,995],{},[33,976,977],{},"No preemption on link failure."," If ",[38,980,600],{}," fails, the transport\nthrows ",[38,983,984],{},"TransportUnavailable"," and the router falls through to BLE. There\nis no retry within ",[38,987,265],{}," — ",[38,990,614],{}," already does its own\ninternal retry loop (with ",[38,993,994],{},"MAX_BUILD_ATTEMPTS = 1"," on the client, more\nif the prewarmer has primed both sides).",[62,997,139],{"id":998},"file-download-path-end-to-end",[29,1000,1001,1002,1005],{},"File downloads over Aware reuse the same data path as chat messages, but\nuse a ",[33,1003,1004],{},"separate OkHttp client"," configured for streaming large files:",[29,1007,1008],{},[191,1009],{"alt":1010,"src":1011},"Diagram 11","\u002Fblog\u002Fwifi-aware-transport\u002Fdiagram-11.svg",[196,1013,1015],{"id":1014},"why-a-separate-client-for-downloads","Why a separate client for downloads?",[29,1017,1018,1019,1022,1023,1026,1027,1030],{},"The chat client (",[38,1020,1021],{},"AwareHttpClientFactory.build",") has a 30 s\n",[38,1024,1025],{},"requestTimeoutMillis"," — appropriate for GraphQL mutations but\ncatastrophic for a 100 MB file download. The download client\n(",[38,1028,1029],{},"buildFileDownload",") sets:",[67,1032,1033,1039,1045,1051],{},[70,1034,1035,1038],{},[38,1036,1037],{},"connectTimeoutMillis = 10_000"," (longer than chat's 5 s, more tolerant\nof slow first-packet on a fresh data path)",[70,1040,1041,1044],{},[38,1042,1043],{},"readTimeout = 120 s"," per read (vs the implicit default of 10 s)",[70,1046,1047,1050],{},[38,1048,1049],{},"requestTimeoutMillis = 120_000"," (2 minutes — enough for most files)",[70,1052,1053,1056],{},[38,1054,1055],{},"retryOnConnectionFailure(true)"," — a dropped mid-download read is\nretried instead of failing the whole transfer",[29,1058,1059,1060,1063,1064,1067],{},"It also ",[33,1061,1062],{},"omits the ChaCha20 interceptor",". The ",[38,1065,1066],{},"\u002Ffs"," endpoint serves raw\nfile bytes (not a signed GraphQL envelope), and the L2 PMK (when present)\nalready encrypts the radio link. Double-encrypting a 50 MB video with\nChaCha20 in software would waste CPU and slow the transfer.",[196,1069,1071],{"id":1070},"streaming-not-buffering","Streaming, not buffering",[29,1073,1074,1075,1078,1079,1082,1083,1086,1087,374,1089,1092,1093,1096],{},"Like the BLE path, Aware downloads stream the file through a\n",[38,1076,1077],{},"ByteReadChannel"," — the file is written to a temp file as bytes arrive,\nnot buffered in memory. ",[38,1080,1081],{},"PeerFileDownloader"," reads 8 KB chunks and emits\nprogress events every second. The same ",[38,1084,1085],{},"DownloadedResponse"," \u002F\n",[38,1088,1081],{},[38,1090,1091],{},"DownloadQueue"," pipeline is reused across all\ntransports — transport-specific only the ",[38,1094,1095],{},"channel"," source.",[62,1098,145],{"id":1099},"prewarming-ble-triggered-aware-startup",[29,1101,1102,1103,1106],{},"The biggest user-visible latency in the Aware path is the ",[33,1104,1105],{},"first\nhandshake"," — if both sides haven't started Aware yet, the user's first\nmessage has to wait for:",[436,1108,1109,1112,1115,1118,1121],{},[70,1110,1111],{},"Local Aware session attach (~1 s)",[70,1113,1114],{},"Local publish + subscribe start (~1 s)",[70,1116,1117],{},"Remote peer's Aware startup (~2 s over BLE)",[70,1119,1120],{},"Mutual discovery (~1 s)",[70,1122,1123],{},"NDP handshake (~400 ms)",[29,1125,1126,1127,1130,1131,1134,1135,1138],{},"That's ~5 seconds before the first byte is sent. To hide this latency,\n",[38,1128,1129],{},"PeerTransportPrewarmer"," runs on ",[38,1132,1133],{},"ChatPage"," entry and ",[33,1136,1137],{},"triggers the\nremote peer's Aware startup via BLE",":",[29,1140,1141],{},[191,1142],{"alt":1143,"src":1144},"Diagram 12","\u002Fblog\u002Fwifi-aware-transport\u002Fdiagram-12.svg",[196,1146,1148],{"id":1147},"the-dual-role-of-ble","The dual role of BLE",[29,1150,1151],{},"BLE serves two purposes here:",[436,1153,1154,1169],{},[70,1155,1156,1159,1160,1163,1164,1168],{},[33,1157,1158],{},"Read the peer's current Aware state"," (cheap, no GATT connect — the\nscan response's ",[38,1161,1162],{},"serviceData"," byte",[1165,1166,1167],"span",{},"0"," carries the Aware flags).",[70,1170,1171,1174,1175,1178,1179,1182,1183,1186],{},[33,1172,1173],{},"Trigger the peer to start Aware"," if it supports but isn't currently\nrunning it. This goes through the regular ",[38,1176,1177],{},"BleTransport.send"," path —\na ",[38,1180,1181],{},"startAware"," GraphQL mutation encrypted with the shared ChaCha20\nkey, delivered via GATT RPC to the peer's ",[38,1184,1185],{},"\u002Fpeer_graphql"," endpoint.",[29,1188,1189,1190,1193],{},"This is one of the few places where the transports cooperate rather\nthan just fall back: BLE is used to ",[33,1191,1192],{},"pre-emptively upgrade"," the\nsession to the faster Aware transport, before the user even notices.",[196,1195,1197,1198,849],{"id":1196},"why-optimistic-setawarerunningtrue","Why optimistic ",[38,1199,1200],{},"setAwareRunning(true)",[29,1202,539,1203,1205,1206,1209,1210,1213,1214,1217],{},[38,1204,1181],{}," mutation returns success as soon as the remote peer's\nresolver invokes ",[38,1207,1208],{},"WifiAwareTransport.start()"," — but the Aware session\nisn't actually attached yet (",[38,1211,1212],{},"onAttached"," fires asynchronously). PlainApp\nmarks the peer as ",[38,1215,1216],{},"awareRunning = true"," optimistically, because:",[67,1219,1220,1226,1238],{},[70,1221,1222,1223,1225],{},"If it actually started, the next ",[38,1224,265],{}," will use Aware (fast).",[70,1227,1228,1229,1231,1232,1234,1235,1237],{},"If it didn't (e.g. peer's Wi-Fi is off), the next ",[38,1230,265],{},"'s ",[38,1233,600],{},"\nwill fail with ",[38,1236,984],{}," and fall back to BLE naturally.",[70,1239,1240,1241,1244],{},"The cost of a false positive is one ~5 s timeout, not a permanent\nblock — ",[38,1242,1243],{},"PeerCircuitBreaker"," records the failure but doesn't open the\nBLE leg (BLE only opens on its own failures).",[196,1246,1248],{"id":1247},"why-throttle-to-30-s","Why throttle to 30 s?",[29,1250,1251,1254],{},[38,1252,1253],{},"PeerTransportPrewarmer.prewarm(peerId)"," records a timestamp per peer\nand refuses to re-run within 30 s. This is because the user navigates\nback and forth between chat list and chat page frequently — without\nthrottling, every navigation would trigger a BLE scan + startAware\nmutation, draining battery and spamming the BLE radio. The 30 s window\nis short enough to catch a peer that just came online (e.g. user opened\nthe app on the remote device) but long enough to avoid spurious re-runs.",[62,1256,151],{"id":1257},"failure-modes-the-fast-skip-flag",[29,1259,1260,1261,1264,1265,1267,1268,1270],{},"Aware has more failure modes than any other transport. The\n",[38,1262,1263],{},"isAwareRunning"," fast-skip flag is the single most important\noptimization in the whole module — without it, every ",[38,1266,265],{}," would\nwaste 10 s on ",[38,1269,600],{}," timing out before falling back to BLE.",[29,1272,1273],{},[191,1274],{"alt":1275,"src":1276},"Diagram 13","\u002Fblog\u002Fwifi-aware-transport\u002Fdiagram-13.svg",[196,1278,539,1280,1282],{"id":1279},"the-isawarerunning-flag-is-the-linchpin",[38,1281,1263],{}," flag is the linchpin",[29,1284,1285,1286,1288],{},"Without this single boolean, every Aware ",[38,1287,265],{}," would either:",[67,1290,1291,1297],{},[70,1292,1293,1294,1296],{},"Always attempt ",[38,1295,600],{}," → 10 s timeout on every send to a peer whose\nAware isn't running.",[70,1298,1299],{},"Always skip Aware → never use it even when both sides have it running.",[29,1301,1302],{},"The flag is refreshed from two sources, in order of authority:",[436,1304,1305,1321],{},[70,1306,1307,1310,1311,1314,1315,1317,1318,1320],{},[33,1308,1309],{},"BLE scan response"," (cheap, no GATT connect) — set by\n",[38,1312,1313],{},"PeerTransportPrewarmer.refreshAwareFlagFromScan",". The peer advertises\nits Aware state in the 9-byte ",[38,1316,1162],{}," payload (byte",[1165,1319,1167],{}," bitfield).",[70,1322,1323,1326,1327,1330],{},[33,1324,1325],{},"GATT DISCOVER reply"," (authoritative) — set by\n",[38,1328,1329],{},"PairingTransport.scanAndDiscover"," when a full discovery happens. This\noverwrites the scan hint.",[29,1332,1333,1334,219,1336,1338,1339,959,1341,1344],{},"When false, ",[38,1335,724],{},[38,1337,269],{}," throw\n",[38,1340,984],{},[33,1342,1343],{},"immediately"," — no scan, no handshake, no\ntimeout. The router falls through to BLE in microseconds.",[62,1346,157],{"id":1347},"key-constants-reference",[1349,1350,1351,1370],"table",{},[1352,1353,1354],"thead",{},[1355,1356,1357,1361,1364,1367],"tr",{},[1358,1359,1360],"th",{},"Constant",[1358,1362,1363],{},"Value",[1358,1365,1366],{},"Where",[1358,1368,1369],{},"Purpose",[1371,1372,1373,1392,1408,1427,1441,1456,1471,1486,1501,1518,1534,1549,1567,1582,1595,1610,1624,1639,1654,1673,1688],"tbody",{},[1355,1374,1375,1381,1386,1389],{},[1376,1377,1378],"td",{},[38,1379,1380],{},"AwareSession.SERVICE_NAME",[1376,1382,1383],{},[38,1384,1385],{},"\"plain-peer\"",[1376,1387,1388],{},"Discovery",[1376,1390,1391],{},"Service name published & subscribed by every PlainApp device",[1355,1393,1394,1399,1402,1405],{},[1376,1395,1396],{},[38,1397,1398],{},"AwareSession.PEER_HANDLE_MAX_AGE_MS",[1376,1400,1401],{},"30 000",[1376,1403,1404],{},"PeerHandle cache",[1376,1406,1407],{},"Discard stale handles (peer's publish session may have been restarted)",[1355,1409,1410,1415,1418,1421],{},[1376,1411,1412],{},[38,1413,1414],{},"AwareSession.READY_TIMEOUT_MS",[1376,1416,1417],{},"15 000",[1376,1419,1420],{},"Handshake",[1376,1422,1423,1424,1426],{},"Subscriber wait for publisher's ",[38,1425,542],{}," receipt",[1355,1428,1429,1434,1436,1438],{},[1376,1430,1431],{},[38,1432,1433],{},"AwareSession.MSG_HELLO",[1376,1435,1167],{},[1376,1437,1420],{},[1376,1439,1440],{},"Subscriber → Publisher message ID",[1355,1442,1443,1448,1451,1453],{},[1376,1444,1445],{},[38,1446,1447],{},"AwareSession.MSG_READY",[1376,1449,1450],{},"1",[1376,1452,1420],{},[1376,1454,1455],{},"Publisher → Subscriber message ID",[1355,1457,1458,1463,1465,1468],{},[1376,1459,1460],{},[38,1461,1462],{},"AwarePeerLink.MAX_BUILD_ATTEMPTS",[1376,1464,1450],{},[1376,1466,1467],{},"Handshake (client only)",[1376,1469,1470],{},"Single attempt — was 3, now 1 because prewarmer primes both sides",[1355,1472,1473,1478,1481,1483],{},[1376,1474,1475],{},[38,1476,1477],{},"AwarePeerLink.ATTEMPT_TIMEOUT_MS",[1376,1479,1480],{},"5 000",[1376,1482,1420],{},[1376,1484,1485],{},"Per-attempt timeout — was 10 s, halved to speed fallback",[1355,1487,1488,1493,1496,1498],{},[1376,1489,1490],{},[38,1491,1492],{},"AwarePeerLink.RETRY_DELAY_MS",[1376,1494,1495],{},"500",[1376,1497,1420],{},[1376,1499,1500],{},"Delay between retry attempts (client only)",[1355,1502,1503,1508,1510,1513],{},[1376,1504,1505],{},[38,1506,1507],{},"AwarePeerLink.REQUEST_TIMEOUT_MS",[1376,1509,1401],{},[1376,1511,1512],{},"NDP",[1376,1514,1515,1517],{},[38,1516,473],{}," timeout",[1355,1519,1520,1525,1528,1531],{},[1376,1521,1522],{},[38,1523,1524],{},"AwareLinkPool.IDLE_TIMEOUT_MS",[1376,1526,1527],{},"60 000",[1376,1529,1530],{},"Pool sweep",[1376,1532,1533],{},"Close idle links after 60 s of inactivity",[1355,1535,1536,1541,1544,1546],{},[1376,1537,1538],{},[38,1539,1540],{},"AwareLinkPool.IDLE_SWEEP_INTERVAL_MS",[1376,1542,1543],{},"10 000",[1376,1545,1530],{},[1376,1547,1548],{},"Sweep interval",[1355,1550,1551,1556,1561,1564],{},[1376,1552,1553],{},[38,1554,1555],{},"AwareHttpClientFactory.AWARE_HOST",[1376,1557,1558],{},[38,1559,1560],{},"\"plain-aware-peer\"",[1376,1562,1563],{},"DNS",[1376,1565,1566],{},"Sentinel hostname resolved to peer IPv6 by custom Dns",[1355,1568,1569,1575,1577,1579],{},[1376,1570,1571,1574],{},[38,1572,1573],{},"build"," chat client",[1376,1576],{},[1376,1578],{},[1376,1580,1581],{},"connectTimeout 5 s, requestTimeout 30 s, ChaCha20 interceptor",[1355,1583,1584,1588,1590,1592],{},[1376,1585,1586],{},[38,1587,1029],{},[1376,1589],{},[1376,1591],{},[1376,1593,1594],{},"connectTimeout 10 s, readTimeout 120 s, requestTimeout 120 s, no crypto",[1355,1596,1597,1602,1604,1607],{},[1376,1598,1599],{},[38,1600,1601],{},"PeerTransportPrewarmer.PREWARM_TTL_MS",[1376,1603,1401],{},[1376,1605,1606],{},"Prewarm",[1376,1608,1609],{},"Throttle per peer",[1355,1611,1612,1617,1619,1621],{},[1376,1613,1614],{},[38,1615,1616],{},"PeerTransportPrewarmer.BLE_SCAN_TIMEOUT_MS",[1376,1618,1417],{},[1376,1620,1606],{},[1376,1622,1623],{},"BLE scan timeout for refreshAwareFlagFromScan",[1355,1625,1626,1631,1633,1636],{},[1376,1627,1628],{},[38,1629,1630],{},"PeerCircuitBreaker.WINDOW_MS",[1376,1632,1401],{},[1376,1634,1635],{},"Circuit breaker",[1376,1637,1638],{},"Open duration after threshold",[1355,1640,1641,1646,1649,1651],{},[1376,1642,1643],{},[38,1644,1645],{},"PeerCircuitBreaker.MAX_FAILURES",[1376,1647,1648],{},"2",[1376,1650,1635],{},[1376,1652,1653],{},"Failures within window to open",[1355,1655,1656,1661,1664,1667],{},[1376,1657,1658],{},[38,1659,1660],{},"TempData.httpsPort",[1376,1662,1663],{},"8443 (default)",[1376,1665,1666],{},"Server",[1376,1668,1669,1670],{},"Publisher's port advertised via ",[38,1671,1672],{},"WifiAwareNetworkSpecifier.setPort",[1355,1674,1675,1680,1683,1685],{},[1376,1676,1677],{},[38,1678,1679],{},"BleServiceData.AWARE_SUPPORTED",[1376,1681,1682],{},"0x01",[1376,1684,1309],{},[1376,1686,1687],{},"Bit indicating peer supports Wi-Fi Aware",[1355,1689,1690,1695,1698,1700],{},[1376,1691,1692],{},[38,1693,1694],{},"BleServiceData.AWARE_RUNNING",[1376,1696,1697],{},"0x02",[1376,1699,1309],{},[1376,1701,1702],{},"Bit indicating peer's Aware service is currently running",[62,1704,163],{"id":1705},"design-trade-offs-recap",[29,1707,1708],{},[191,1709],{"alt":1710,"src":1711},"Diagram 14","\u002Fblog\u002Fwifi-aware-transport\u002Fdiagram-14.svg",[62,1713,1715],{"id":1714},"further-reading","Further Reading",[67,1717,1718,1730,1735],{},[70,1719,1720,1722,1723,1725,1726,1729],{},[46,1721,49],{"href":48}," — how ",[38,1724,232],{},"\nfits into the ",[38,1727,1728],{},"LAN → Aware → BLE"," fallback chain and the broader chat\nsend\u002Freceive pipeline.",[70,1731,1732,1734],{},[46,1733,54],{"href":53}," — the last-resort transport that\ntakes over when Aware is unavailable; also the channel used by the\nprewarmer to trigger Aware startup on the remote peer.",[70,1736,1737,1739,1740,374,1743,1746],{},[46,1738,59],{"href":58}," — how the shared ChaCha20 key reused\nas the Aware PMK is established, and how the BLE scan response flags\n(",[38,1741,1742],{},"AWARE_SUPPORTED",[38,1744,1745],{},"AWARE_RUNNING",") are populated.\n\n",{"title":1748,"searchDepth":1749,"depth":1749,"links":1750},"",3,[1751,1753,1756,1759,1763,1766,1770,1774,1778,1784,1788,1791,1795,1801,1805,1806,1807],{"id":64,"depth":1752,"text":65},2,{"id":166,"depth":1752,"text":75,"children":1754},[1755],{"id":198,"depth":1749,"text":199},{"id":255,"depth":1752,"text":81,"children":1757},[1758],{"id":279,"depth":1749,"text":280},{"id":297,"depth":1752,"text":87,"children":1760},[1761,1762],{"id":333,"depth":1749,"text":334},{"id":362,"depth":1749,"text":363},{"id":392,"depth":1752,"text":93,"children":1764},[1765],{"id":419,"depth":1749,"text":420},{"id":463,"depth":1752,"text":99,"children":1767},[1768,1769],{"id":501,"depth":1749,"text":502},{"id":571,"depth":1749,"text":572},{"id":617,"depth":1752,"text":105,"children":1771},[1772],{"id":631,"depth":1749,"text":1773},"What the onUnavailable callback means",{"id":672,"depth":1752,"text":111,"children":1775},[1776,1777],{"id":692,"depth":1749,"text":693},{"id":757,"depth":1749,"text":758},{"id":777,"depth":1752,"text":1779,"children":1780},"IPv6 Addressing & the plain-aware-peer DNS Trick",[1781,1782],{"id":822,"depth":1749,"text":823},{"id":844,"depth":1749,"text":1783},"Why network.socketFactory?",{"id":872,"depth":1752,"text":127,"children":1785},[1786,1787],{"id":898,"depth":1749,"text":899},{"id":909,"depth":1749,"text":910},{"id":932,"depth":1752,"text":133,"children":1789},[1790],{"id":944,"depth":1749,"text":945},{"id":998,"depth":1752,"text":139,"children":1792},[1793,1794],{"id":1014,"depth":1749,"text":1015},{"id":1070,"depth":1749,"text":1071},{"id":1099,"depth":1752,"text":145,"children":1796},[1797,1798,1800],{"id":1147,"depth":1749,"text":1148},{"id":1196,"depth":1749,"text":1799},"Why optimistic setAwareRunning(true)?",{"id":1247,"depth":1749,"text":1248},{"id":1257,"depth":1752,"text":151,"children":1802},[1803],{"id":1279,"depth":1749,"text":1804},"The isAwareRunning flag is the linchpin",{"id":1347,"depth":1752,"text":157},{"id":1705,"depth":1752,"text":163},{"id":1714,"depth":1752,"text":1715},"Transport","2025-01-30","This article explains how PlainApp uses Wi-Fi Aware (NAN — Neighbor Awareness Networking) as the middle tier of its peer transport fallback chain, between LAN (same-subnet HTTPS) and BLE (last-resort GATT RPC). Wi-Fi Aware is what makes two PlainApp devices talk when they are on different SSIDs, guest vs IoT VLANs, or no Wi-Fi infrastructure at all — without ever needing an IP address from a DHCP server.","md",{},true,"\u002Fblog\u002Fwifi-aware-transport","19 min read",{"title":24,"description":1810},"How can two phones talk on different Wi-Fi networks without an IP? PlainApp uses Wi-Fi Aware (NAN) for discovery, handshake, and data transfer — no DHCP needed.","Wi-Fi Aware (NAN): Device-to-Device Data Transfer Without IP","blog\u002Fwifi-aware-transport","4IN4yBV-dbXKL1zaeiiVIGcq8aA68cTQInGmtnzLkcs",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":1822},"\u003Cpath fill=\"currentColor\" d=\"M224 128a8 8 0 0 1-8 8H59.31l58.35 58.34a8 8 0 0 1-11.32 11.32l-72-72a8 8 0 0 1 0-11.32l72-72a8 8 0 0 1 11.32 11.32L59.31 120H216a8 8 0 0 1 8 8\"\u002F>",1788009009902]