Der Artikel behandelt den Android-only Aware-Session-Lebenszyklus, das
Publish-/Subscribe-Discovery-Modell, den zweiphasigen Role-Split-Handshake,
der requestNetwork auf beiden Seiten innerhalb des ~500-ms-Fensters des
Frameworks synchronisiert, den Per-Peer-Link-Pool mit Idle-Sweeping, den
IPv6- + Custom-DNS-Trick, der einem einzigen OkHttp-Client erlaubt, sowohl LAN
als auch Aware zu bedienen, und den Prewarmer, der den Aware-Start des Peers
über BLE triggert.
Für die übergeordnete Fallback-Kette siehe Chat-Architektur. Für den BLE-Transport, der übernimmt, wenn Aware unavailable ist, siehe BLE-Transport. Wie der gemeinsame ChaCha20-Schlüssel, der als Aware-PMK wiederverwendet wird, etabliert wird, siehe Pairing-Ablauf.
Inhaltsverzeichnis
- Warum Wi-Fi Aware?
- Wo Aware in der Fallback-Kette sitzt
- Session-Lebenszyklus: Attach → Publish + Subscribe
- Discovery & Rollenzuweisung
- Der Zweiphasen-Handshake (hello + ready)
- NDP requestNetwork — das 500-ms-Fenster
- Per-Peer-Link-Pool & Idle-Sweeping
- IPv6-Adressierung & der
plain-aware-peer-DNS-Trick - Kryptografie: PMK-Derivation & ChaCha20-Wiederverwendung
- Sende-Pfad für Nachrichten (End-to-End)
- Download-Pfad für Dateien (End-to-End)
- Prewarming: BLE-getriggerter Aware-Start
- Fehlermodi & das Fast-Skip-Flag
- Referenz der Schlüsselkonstanten
- Zusammenfassung der Design-Trade-offs
Warum Wi-Fi Aware? {#why-wi-fi-aware}
Wi-Fi Aware (IEEE 802.11bc, früher NAN — Neighbor Awareness Networking) ist eine Zertifizierung der Wi-Fi Alliance, die es zwei Geräten erlaubt, sich gegenseitig zu entdecken und Daten auszutauschen ohne jegliche Wi-Fi-Infrastruktur — kein AP, kein Router, kein DHCP. PlainApp verwendet es für zwei Szenarien, die LAN nicht abdecken kann:
- Unterschiedliche SSIDs / VLANs. Ein Telefon im Guest-Netzwerk und ein Laptop im IoT-VLAN sind beide per Wi-Fi „online", können aber einander nicht per IP erreichen. Aware erzeugt einen direkten Device-to-Device-Data-Path, der die Infrastruktur komplett umgeht.
- Gar keine Infrastruktur. Zwei Geräte in der Wildnis mit eingeschaltetem Wi-Fi, aber ohne AP, können trotzdem chatten. (BLE deckt das auch ab, aber Aware ist deutlich schneller — ~10 ms Round Trips vs. Sekunden, und MB/s vs. Zehner-KB/s.)
Plattform-Beschränkungen
Wi-Fi Aware ist in PlainApp nur auf Android verfügbar:
- Android 13 (API 33) ist das Minimum — die
WifiAwareNetworkSpecifier.Builder-Overloads mitsetPort()undsetPmk(), auf die PlainApp angewiesen ist, erfordernisTPlus(). - iOS exponiert Wi-Fi Aware nicht an Dritt-Apps. iOS-PlainApp fällt direkt
von LAN auf BLE; das
WifiAwareTransport-Objekt wird nicht einmal ins iOS-Target kompiliert (@RequiresApi(Build.VERSION_CODES.S)+ androidMain-Source-Set).
Deshalb ruft PeerTransportRouter.buildListcreateWifiAwareTransport() auf — eine Factory, die auf iOS null
zurückgibt.
Wo Aware in der Fallback-Kette sitzt {#where-aware-sits-in-the-fallback-chain}
PlainApps PeerTransportRouter ist eine geordnete Liste. Für jeden send-
oder downloadFile-Aufruf durchläuft er die Liste und versucht jeden
Transport, bis einer erfolgreich ist; Fehler kaskadieren nach unten.
Warum ist Aware „die Mitte" und nicht „der Erste"?
Weil LAN fast immer schneller ist, wenn verfügbar. Ein Same-Subnet-Wi-Fi- Hop über einen AP ist ein einzelner 802.11-Frame-Austausch; ein Aware-Data-Path addiert ein NDP-Setup (~5 s bei Erstnutzung) plus einen zweiten Wi-Fi-Radio-Kontext für den Device-to-Device-Link. Wenn beide erreichbar sind, gewinnt LAN auf Latenz und Durchsatz.
Umgekehrt ist BLE immer langsamer — aber es funktioniert, sobald beide Geräte gepaart sind. Aware liegt in der Mitte: schneller als BLE, langsamer als LAN, und nur auf Android-13+-Geräten mit eingeschaltetem Wi-Fi verfügbar.
Session-Lebenszyklus: Attach → Publish + Subscribe {#session-lifecycle-attach--publish--subscribe}
Eine Wi-Fi Aware-Session ist prozessweit. Es gibt genau eine
WifiAwareSession pro Gerät; darin betreibt PlainApp eine Publish-Session
(sodass Peers uns entdecken können) und eine Subscribe-Session (sodass
wir Peers entdecken können). Beide werden gestartet, sobald
AwareSession.start() den attach-Callback abschließt.
Warum Publish UND Subscribe auf demselben Gerät?
Das Wi-Fi Aware-Discovery-Modell ist asymmetrisch: ein Publisher
advertised einen Service, ein Subscriber scannt danach. Um Discovery
symmetrisch zu machen (beide Geräte entdecken einander), macht PlainApp
beides gleichzeitig. Ohne das müsste Gerät A im Voraus wissen, ob es für
einen gegebenen Peer der Publisher oder der Subscriber ist — aber Peer-Rollen
werden später per clientId-Vergleich bestimmt (siehe
Discovery & Rollenzuweisung).
Gleichzeitiges Publishen und Subscriben bedeutet, dass jedes Gerät das
onServiceDiscovered des jeweils anderen (als Subscriber) UND die
Hello-Nachrichten des jeweils anderen (als Publisher) empfängt — beide
Richtungen des Handshakes sind immer verfügbar.
Auto-Restart bei Termination
Einige Android-Varianten (insbesondere MIUI) beenden lange laufende
Aware-Sessionen, um Akku zu sparen. PlainApp behandelt das in den
onSessionTerminated-Callbacks: Er nullt die beendete Session und ruft
publishOwnService / subscribeOwnService sofort wieder auf der noch
angehängten WifiAwareSession auf. Die Attach-Session selbst geht nicht
verloren — nur die Publish/Subscribe-Discovery-Session. Peer-Handles von vor
der Termination werden stale, weshalb awaitPeerHandle den
discoveredAt-Zeitstempel prüft und Handles verwirft, die älter als 30 s
sind.
Discovery & Rollenzuweisung {#discovery--role-assignment}
Das Wi-Fi Aware-Data-Path-Protokoll verlangt, dass eine Seite als Publisher (Server) und die andere als Subscriber (Client) agiert. Beide Seiten können nicht gleichzeitig der Initiator sein — das Framework lehnt Anfragen ohne passendes Gegenstück ab.
PlainApp weist Rollen deterministisch pro Peer zu, über einen einfachen lexikografischen Vergleich von clientIds:
Warum deterministisch und nicht verhandelt?
Ein verhandelter Ansatz (z.B. „niedrigere MAC ist der Server") würde einen
zusätzlichen Nachrichtenaustausch erfordern. Der lexikografische Vergleich ist
idempotent, symmetrisch und zustandslos: Beide Geräte berechnen
dieselbe Rolle für dasselbe Paar ohne jegliche Kommunikation. Die clientId
ist eine 13-Zeichen-kurze UUID, sodass Gleichstände (clientId == peer.id)
nur beim Vergleich eines Peers mit sich selbst auftreten — was nie die
Transportebene erreicht.
Die Rolle bestimmt zwei Dinge flussabwärts:
- Wer die Retry-Schleife treibt. Nur der Client retried
requestNetwork; der Server macht genau einen Versuch pro empfangenem Hello. Das ist kritisch für das 500-ms-Fenster (nächster Abschnitt). - Wer den Port setzt. Der Publisher ruft
setPort(httpsPort)auf, weil er derjenige ist, der eingehende Verbindungen auf seinem HTTPS-Server-Port akzeptiert. Der Subscriber setzt keinen Port — er erfährt den Port des Peers aus denWifiAwareNetworkInfo, nachdem der Data Path etabliert ist.
Der Zweiphasen-Handshake (hello + ready) {#the-two-phase-handshake-hello--ready}
Der schwierigste Teil des Wi-Fi Aware-Data-Path-Setups ist das Timing.
Das Framework verlangt, dass beide Seiten connectivityManager.requestNetwork
innerhalb von etwa 500 ms nacheinander aufrufen — ruft eine Seite es auf,
bevor die andere ihre passende Anfrage registriert hat, lehnt das Framework es
sofort mit onUnavailable
(„releaseRequestAsUnfulfillableByAnyFactory") ab.
PlainApp löst das mit einem Zwei-Nachrichten-Application-Layer-Handshake,
der auf dem Aware-L2-Message-Channel läuft (derselbe sendMessage-API, der
auch von onServiceDiscovered verwendet wird):
Warum zwei Nachrichten (hello + ready) statt nur einer?
Das Hello allein reicht wegen der Richtungsasymmetrie nicht aus. Der
Subscriber kann Hello in dem Moment senden, in dem er den Publisher entdeckt
(in onServiceDiscovered), aber der Publisher kann requestNetwork nicht
starten, bevor er das PeerHandle des Subscribers hat, das er erst durch
Empfang des Hellos erfährt. Das Hello erfüllt also zwei Zwecke:
- Dem Publisher das PeerHandle des Subscribers liefern. Der Publisher
braucht es, um den
WifiAwareNetworkSpecifierzu bauen. - Verbindungswillen signalisieren. Der Empfang des Hellos teilt dem Publisher mit: „Der Subscriber wird gleich requestNetwork aufrufen, das sollte ich auch tun."
Die ready-Quittung existiert für die entgegengesetzte Richtung — um dem
Subscriber mitzuteilen: „Der Publisher hat sein requestNetwork registriert."
Ohne sie könnte das requestNetwork des Subscribers dem des Publishers
zuvorkommen und vom Framework abgelehnt werden. Die ready-Quittung ist ein
nicht-blockierendes Signal: Der Subscriber wartet nicht darauf, bevor er
requestNetwork aufruft (das würde einen Round-Trip addieren), aber wenn sie
ankommt, während der Subscriber im IDLE-Zustand ist (zwischen
Retry-Versuchen), kann der Subscriber sofort retrien, ohne auf die
RETRY_DELAY_MS-Lücke zu warten.
Die Retry-Schleifen-Asymmetrie
Das ist der subtilste Teil des Designs. Nur der Subscriber retried. Der
Publisher macht genau einen requestNetwork-Versuch pro empfangenem Hello.
Das liegt daran, dass:
- Wenn beide Seiten unabhängig retrien würden, würden ihre Retry-Zyklen
außer Phase driften (unterschiedliche
delay()-Dauern, unterschiedliche GC-Pausen), und die beidenrequestNetwork-Aufrufe würden selten innerhalb des 500-ms-Fensters überlappen. - Die Retry-Schleife des Subscribers sendet bei jedem Versuch ein frisches
Hello, was den
buildLinkdes Publishers überpublishHelloListenerserneut triggert. Das garantiert, dass dasrequestNetworkdes Publishers dem Hello immer um ~50 ms folgt, wohl innerhalb des 500-ms-Fensters.
Das ist detailliert in
AwarePeerLink.build
dokumentiert.
NDP requestNetwork — das 500-ms-Fenster {#ndp-requestnetwork--the-500-ms-window}
Der requestNetwork-Aufruf ist die timing-kritischste Operation im
Aware-Transport. Hier ist, was auf jeder Seite passiert:
Was der onUnavailable-Callback bedeutet
onUnavailable feuert, wenn das Framework das requestNetwork ablehnt, bevor
es eine passende Peer-Anfrage findet. Das PeerHandle selbst ist weiterhin
gültig — nur das NDP (Neighbor Discovery Protocol)-Pairing ist
fehlgeschlagen, weil die andere Seite noch nicht registriert war. PlainApp
ruft in diesem Fall bewusst nicht session.invalidatePeerHandle auf, da
das Invalidieren des Handles das einzige Signal verwerfen würde, dass
onServiceDiscovered jemals aufgerufen wurde (es feuert einmal pro Peer pro
Subscribe-Session-Lebenszeit). Mit erhaltenem Handle kann der Retry es
wiederverwenden, statt auf eine frische Discovery zu warten.
Dasselbe gilt für das publisher-seitige Handle aus onMessageReceived — der
Publisher behält den publishPeerHandles[fromCid]-Eintrag über fehlgeschlagene
Versuche hinweg, sodass das nächste Hello des Subscribers das gecachte Handle
wiederverwendet, statt verworfen zu werden.
Per-Peer-Link-Pool & Idle-Sweeping {#per-peer-link-pool--idle-sweeping}
Jeder gepaarte Peer erhält sein eigenes AwarePeerLink-Objekt, das der
prozessweite AwareLinkPool besitzt. Der Pool behandelt Discovery-Events,
Link-Wiederverwendung und Idle-Eviction.
Warum kein Auto-Build bei Discovery?
Der Pool baut explizit keinen Link, wenn onServiceDiscovered feuert. Das
ist eine kritische Entscheidung: Ein belebtes Café könnte 100 PlainApp-Geräte
haben, die alle den „plain-peer"-Service publishen. Würde jede Discovery ein
requestNetwork triggern, wäre das Framework mit NDP-Setup-Versuchen
überflutet und das Wi-Fi-Radio saturiert.
Stattdessen zeichnet der Pool nur das PeerHandle auf und wartet auf eines von:
- Der lokale Nutzer sendet eine Nachricht →
WifiAwareTransport.send→pool.buildLink(peer)(Sender-seitiger Trigger). - Der Remote-Peer sendet ein Hello →
onPublishHelloReceived→buildLink(peer)(Empfänger-seitiger Trigger). - Der Remote-Peer sendet ein Ready →
onSubscribeReadyReceived→buildLink(peer)(Empfänger-seitiger Trigger).
So werden Links nur für Peers gebaut, mit denen der Nutzer tatsächlich Nachrichten austauscht — nicht für jedes PlainApp-Gerät in Funkreichweite.
Idle-Sweep
Alle 10 Sekunden durchläuft der Pool alle Links und schließt jene, deren
lastActiveAt älter als 60 Sekunden ist. Jeder send- und
downloadFile-Aufruf ruft link.touch() auf, um den Zeitstempel
aufzufrischen. Das gibt den Wi-Fi-Radio-Kontext und den OkHttp-Connection-Pool
für Peers frei, mit denen der Nutzer aufgehört hat zu chatten — wichtig, weil
Android die Anzahl simultaner Aware-Data-Paths auf etwa 4–10 limitiert
(geräteabhängig).
IPv6-Adressierung & der plain-aware-peer-DNS-Trick {#ipv6-addressing--the-plain-aware-peer-dns-trick}
Wi-Fi Aware-Data-Paths verwenden nur Link-Local-IPv6. Es gibt kein IPv4,
keinen DNS-Server, kein DHCP. Die IPv6-Adresse des Peers wird über das
WifiAwareNetworkInfo.peerIpv6Addr-Feld in onCapabilitiesChanged
geliefert — eine fe80::...-Adresse, die nur auf dem
Aware-Netzwerkinterface Bedeutung hat.
PlainApp muss HTTPS-Anfragen an diese Adresse senden, aber OkHttps
https://-URL-Parsing lehnt rohe IPv6-Literale in einem Hostnamen ab
(https://[fe80::abcd]:8443/ funktioniert, aber das Routing über einen
Custom-Dns-Resolver ist sauberer). Der Trick:
Warum ein Sentinel-Hostname?
Die Alternative — das IPv6-Literal direkt in der URL übergeben — würde
erfordern, dass jede Aufrufstelle die Link-Local-Adresse kennt. Durch die
Verwendung eines Sentinel-Hostnamens ist die URL-Konstruktion für LAN und
Aware identisch: Beide produzieren eine gültige
https://<host>:<port>/peer_graphql-URL, die OkHttp parsen kann. Der
einzige Unterschied ist die an den Client gebundene Dns-Implementierung —
LAN verwendet das System-DNS, Aware verwendet awareDns(peerIpv6), das die
gecachte Link-Local-Adresse für den Sentinel-Hostnamen zurückgibt und für
alles andere auf Dns.SYSTEM durchfällt.
Warum network.socketFactory?
Androids Network-Objekt repräsentiert ein spezifisches Netzwerkinterface (in
diesem Fall der Aware-Data-Path). Indem wir network.socketFactory aufrufen
und es an OkHttps socketFactory-Config übergeben, erzwingen wir, dass alle
TCP-Sockets auf dem Aware-Interface erzeugt werden — nicht auf dem
Standard-Wi-Fi- oder Cellular-Interface. Ohne das würde das OS die Anfrage
über das Default-Netzwerk routen, wo das Link-Local-IPv6 unreachable ist,
und die Anfrage würde mit ENETUNREACH fehlschlagen.
Kryptografie: PMK-Derivation & ChaCha20-Wiederverwendung {#cryptography-pmk-derivation--chacha20-reuse}
Wi-Fi Aware unterstützt einen optionalen PMK (Pairwise Master Key) für den Data Path. Wenn gesetzt, ist der L2-Link selbst mit diesem PMK verschlüsselt — das Wi-Fi-Radio übernimmt die Verschlüsselung, keine Application-Layer-Crypto nötig.
PlainApp leitet den PMK aus demselben ChaCha20-gemeinsamen Schlüssel ab,
den LanTransport und BleTransport für die Application-Layer-Verschlüsselung
verwenden:
Warum auf 32 Bytes abschneiden?
Der Wi-Fi Aware-PMK muss exakt 32 Bytes (256 Bits) betragen. Der
ChaCha20-gemeinsame Schlüssel aus dem Pairing ist im Normalfall ebenfalls
32 Bytes, also ist der raw.size == 32-Zweig der Common Path. Der
Truncation-/Padding-Fallback behandelt den (theoretischen) Fall, dass der
Schlüssel kürzer gespeichert wurde — Padding mit Nullen auf 32 Bytes ist eine
defensive Maßnahme, die in der Praxis bei korrekt gepaarten Peers nicht
vorkommt.
Der signierte Umschlag ist identisch zu LAN
Weil createCryptoHttpClient dieselbe Factory ist, die auch von
LanTransport verwendet wird, ist die L7-Crypto auf Aware byte-für-byte
identisch zu LAN. Das serverseitige PeerGraphQLService weiß nicht (oder
interessiert sich nicht dafür), welcher Transport die Anfrage geliefert hat —
es sieht einfach eine signierte, verschlüsselte GraphQL-Nutzlast und
entschlüsselt sie mit dem gemeinsamen Schlüssel des Peers. Das ist das
„eine Codebasis, viele Transporte"-Prinzip, das in
Chat-Architektur dokumentiert ist.
Sende-Pfad für Nachrichten (End-to-End) {#message-send-path-end-to-end}
Alles zusammengeführt — was passiert, wenn eine Chat-Nachricht über Wi-Fi Aware gesendet wird:
Beachtenswerte Designentscheidungen
- Verbindungswiederverwendung. Anders als
BleTransport, das die GATT-Verbindung nach jeder Anfrage abbaut, verwendetWifiAwareTransportden Aware-Data-Path für so viele Anfragen weiter, wie der Nutzer innerhalb des 60-s-Idle-Fensters macht. Die erste Anfrage zahlt den ~400-ms-Handshake; nachfolgende Anfragen sind ~10-ms-Round-Trips. - Dieselbe Crypto wie LAN. Der ChaCha20-Interceptor und der signierte
Umschlag sind byte-identisch zu LAN. Das
PeerGraphQLServicedes Peers weiß nicht, welcher Transport die Anfrage geliefert hat. - Keine Preemption bei Link-Versagen. Schlägt
buildLinkfehl, wirft der TransportTransportUnavailableund der Router fällt auf BLE durch. Es gibt kein Retry innerhalb vonsend—AwarePeerLink.buildmacht bereits seine eigene interne Retry-Schleife (mitMAX_BUILD_ATTEMPTS = 1auf dem Client, mehr, wenn der Prewarmer beide Seiten geprimed hat).
Download-Pfad für Dateien (End-to-End) {#file-download-path-end-to-end}
Datei-Downloads über Aware wiederverwenden denselben Data-Path wie Chat-Nachrichten, verwenden aber einen separaten OkHttp-Client, der für das Streamen großer Dateien konfiguriert ist:
Warum ein separater Client für Downloads?
Der Chat-Client (AwareHttpClientFactory.build) hat ein 30-s-requestTimeoutMillis
— angemessen für GraphQL-Mutationen, aber katastrophal für einen 100-MB-
Datei-Download. Der Download-Client (buildFileDownload) setzt:
connectTimeoutMillis = 10_000(länger als Chats 5 s, tolerant gegenüber langsamem First-Packet auf einem frischen Data Path)readTimeout = 120 spro Read (vs. der implizite Default von 10 s)requestTimeoutMillis = 120_000(2 Minuten — genug für die meisten Dateien)retryOnConnectionFailure(true)— ein abgebrochener Mid-Download-Read wird retried, statt die ganze Übertragung fehlschlagen zu lassen
Er lässt auch den ChaCha20-Interceptor weg. Der /fs-Endpunkt dient rohe
Datei-Bytes (keinen signierten GraphQL-Umschlag), und der L2-PMK (wenn
vorhanden) verschlüsselt bereits den Funklink. Ein 50-MB-Video zusätzlich mit
ChaCha20 in Software zu verschlüsseln, würde CPU verschwenden und die
Übertragung verlangsamen.
Streaming, nicht Puffern
Wie der BLE-Pfad streamen Aware-Downloads die Datei durch einen
ByteReadChannel — die Datei wird beim Eintreffen der Bytes in eine
Temp-Datei geschrieben, nicht im Speicher gepuffert. PeerFileDownloader
liest 8-KB-Chunks und emittet jede Sekunde Progress-Events. Dieselbe
DownloadedResponse- / PeerFileDownloader- / DownloadQueue-Pipeline
wird über alle Transporte wiederverwendet — transport-spezifisch ist nur die
channel-Quelle.
Prewarming: BLE-getriggerter Aware-Start {#prewarming-ble-triggered-aware-startup}
Die größte nutzersichtbare Latenz im Aware-Pfad ist der erste Handshake — wenn beide Seiten Aware noch nicht gestartet haben, muss die erste Nachricht des Nutzers warten auf:
- Lokale Aware-Session-Attach (~1 s)
- Lokaler Publish + Subscribe-Start (~1 s)
- Aware-Startup des Remote-Peers (~2 s über BLE)
- Gegenseitige Discovery (~1 s)
- NDP-Handshake (~400 ms)
Das sind ~5 Sekunden, bevor das erste Byte gesendet wird. Um diese Latenz zu
verbergen, läuft PeerTransportPrewarmer beim Betreten der ChatPage und
triggert den Aware-Start des Remote-Peers über BLE:
Die Doppelrolle von BLE
BLE erfüllt hier zwei Zwecke:
- Den aktuellen Aware-Status des Peers lesen (billig, kein GATT-Connect —
das
serviceData-Byte0 des Scan-Responses trägt die Aware-Flags). - Den Peer triggern, Aware zu starten, wenn er es unterstützt, aber
aktuell nicht läuft. Das geht über den regulären
BleTransport.send-Pfad — einestartAware-GraphQL-Mutation, verschlüsselt mit dem gemeinsamen ChaCha20-Schlüssel, geliefert via GATT-RPC an den/peer_graphql-Endpunkt des Peers.
Das ist eine der wenigen Stellen, an denen die Transporte kooperieren statt nur fallbacken: BLE wird verwendet, um die Session präventiv auf den schnelleren Aware-Transport zu upgraden, bevor der Nutzer es überhaupt merkt.
Warum optimistisches setAwareRunning(true)?
Die startAware-Mutation gibt Success zurück, sobald der Resolver des
Remote-Peers WifiAwareTransport.start() aufruft — aber die Aware-Session ist
noch nicht tatsächlich attached (onAttached feuert asynchron). PlainApp
markiert den Peer optimistisch als awareRunning = true, weil:
- Wenn sie tatsächlich startete, wird der nächste
sendAware verwenden (schnell). - Wenn nicht (z.B. Wi-Fi des Peers ist aus), wird
buildLinkdes nächstensendmitTransportUnavailablefehlschlagen und natürlich auf BLE zurückfallen. - Die Kosten eines False Positive sind ein ~5-s-Timeout, kein dauerhafter
Block —
PeerCircuitBreakerzeichnet den Fehler auf, öffnet aber nicht das BLE-Leg (BLE öffnet nur bei eigenen Fehlern).
Warum auf 30 s drosseln?
PeerTransportPrewarmer.prewarm(peerId) zeichnet einen Zeitstempel pro Peer
auf und verweigert einen Re-Run innerhalb von 30 s. Das liegt daran, dass der
Nutzer häufig zwischen Chat-Liste und Chat-Seite hin- und hernavigiert — ohne
Drosselung würde jede Navigation einen BLE-Scan + startAware-Mutation
triggern, Akku entladen und das BLE-Radio spammen. Das 30-s-Fenster ist kurz
genug, um einen Peer zu erwischen, der gerade online kam (z.B. Nutzer hat die
App auf dem Remote-Gerät geöffnet), aber lang genug, um spurious Re-Runs zu
vermeiden.
Fehlermodi & das Fast-Skip-Flag {#failure-modes--the-fast-skip-flag}
Aware hat mehr Fehlermodi als jeder andere Transport. Das
isAwareRunning-Fast-Skip-Flag ist die wichtigste Einzeloptimierung im
ganzen Modul — ohne es würde jeder send 10 s mit buildLink-Timeout
verschwenden, bevor auf BLE gefallen wird.
Das isAwareRunning-Flag ist der Dreh- und Angelpunkt
Ohne diesen einzigen Boolean würde jeder Aware-send entweder:
- Immer
buildLinkversuchen → 10-s-Timeout bei jedem Send an einen Peer, dessen Aware nicht läuft. - Immer Aware überspringen → es nie verwenden, selbst wenn beide Seiten es laufen haben.
Das Flag wird aus zwei Quellen aufgefrischt, in der Reihenfolge der Autorität:
- BLE-Scan-Response (billig, kein GATT-Connect) — gesetzt durch
PeerTransportPrewarmer.refreshAwareFlagFromScan. Der Peer advertised seinen Aware-Status in der 9-Byte-serviceData-Nutzlast (Byte0-Bitfield). - GATT-DISCOVER-Antwort (autoritativ) — gesetzt durch
PairingTransport.scanAndDiscover, wenn eine volle Discovery stattfindet. Das überschreibt den Scan-Hint.
Wenn false, werfen WifiAwareTransport.send und downloadFilesofort TransportUnavailable — kein Scan, kein Handshake, kein Timeout.
Der Router fällt in Mikrosekunden auf BLE durch.
Referenz der Schlüsselkonstanten {#key-constants-reference}
| Konstante | Wert | Wo | Zweck |
|---|---|---|---|
AwareSession.SERVICE_NAME | "plain-peer" | Discovery | Service-Name, das von jedem PlainApp-Gerät published & subscribed wird |
AwareSession.PEER_HANDLE_MAX_AGE_MS | 30 000 | PeerHandle-Cache | Stale Handles verwerfen (Publish-Session des Peers könnte neu gestartet sein) |
AwareSession.READY_TIMEOUT_MS | 15 000 | Handshake | Subscriber-Wartezeit auf ready-Quittung des Publishers |
AwareSession.MSG_HELLO | 0 | Handshake | Subscriber → Publisher-Nachrichten-ID |
AwareSession.MSG_READY | 1 | Handshake | Publisher → Subscriber-Nachrichten-ID |
AwarePeerLink.MAX_BUILD_ATTEMPTS | 1 | Handshake (nur Client) | Einzelner Versuch — war 3, jetzt 1, weil Prewarmer beide Seiten primed |
AwarePeerLink.ATTEMPT_TIMEOUT_MS | 5 000 | Handshake | Per-Versuch-Timeout — war 10 s, halbiert, um Fallback zu beschleunigen |
AwarePeerLink.RETRY_DELAY_MS | 500 | Handshake | Verzögerung zwischen Retry-Versuchen (nur Client) |
AwarePeerLink.REQUEST_TIMEOUT_MS | 30 000 | NDP | connectivityManager.requestNetwork-Timeout |
AwareLinkPool.IDLE_TIMEOUT_MS | 60 000 | Pool-Sweep | Idle-Links nach 60 s Inaktivität schließen |
AwareLinkPool.IDLE_SWEEP_INTERVAL_MS | 10 000 | Pool-Sweep | Sweep-Intervall |
AwareHttpClientFactory.AWARE_HOST | "plain-aware-peer" | DNS | Sentinel-Hostname, der durch Custom-Dns zur Peer-IPv6 aufgelöst wird |
build-Chat-Client | connectTimeout 5 s, requestTimeout 30 s, ChaCha20-Interceptor | ||
buildFileDownload | connectTimeout 10 s, readTimeout 120 s, requestTimeout 120 s, keine Crypto | ||
PeerTransportPrewarmer.PREWARM_TTL_MS | 30 000 | Prewarm | Throttle pro Peer |
PeerTransportPrewarmer.BLE_SCAN_TIMEOUT_MS | 15 000 | Prewarm | BLE-Scan-Timeout für refreshAwareFlagFromScan |
PeerCircuitBreaker.WINDOW_MS | 30 000 | Circuit-Breaker | Open-Dauer nach Schwellwert |
PeerCircuitBreaker.MAX_FAILURES | 2 | Circuit-Breaker | Fehler im Fenster bis Open |
TempData.httpsPort | 8443 (Default) | Server | Port des Publishers, advertised via WifiAwareNetworkSpecifier.setPort |
BleServiceData.AWARE_SUPPORTED | 0x01 | BLE-Scan-Response | Bit, das anzeigt, dass der Peer Wi-Fi Aware unterstützt |
BleServiceData.AWARE_RUNNING | 0x02 | BLE-Scan-Response | Bit, das anzeigt, dass der Aware-Service des Peers aktuell läuft |
Zusammenfassung der Design-Trade-offs {#design-trade-offs-recap}
Weiterführende Literatur
- Chat-Architektur — wie
WifiAwareTransportin dieLAN → Aware → BLE-Fallback-Kette und die übergeordnete Chat-Send/Empfangs-Pipeline passt. - BLE-Transport — der Transport der letzten Wahl, der übernimmt, wenn Aware unavailable ist; außerdem der Channel, den der Prewarmer nutzt, um den Aware-Start beim Remote-Peer zu triggern.
- Pairing-Ablauf — wie der gemeinsame ChaCha20-Schlüssel,
der als Aware-PMK wiederverwendet wird, etabliert wird, und wie die
BLE-Scan-Response-Flags (
AWARE_SUPPORTED/AWARE_RUNNING) bevölkert werden.