mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 04:45:21 +00:00
Fix wifi aware routing (#534)
* Background persistence (#505) * persistence step 1 * fix build * messages in the background work, notifications not yet * app state store * DM icon shows up * notification launches when app is closed! * keep ui updated * lifecycle fixes * extensive logging, maybe revert later * send nickname in announcement * quit in notification * setting in about sheet * fix quit bitchat * lifecycle fixes * power mode based on background state * stats for both direciotns * fix graph persistence * better counting * count per device * only compute when debug sheet is open? untested * fix read receipts * fix read receipts fully * fix unread badge if messages have been read in focus * foreground promotion fix * fix app kill in notification * adjust to new tor * nice * about sheet design * bump version 1.6.0 (#524) * Automated update of relay data - Sun Dec 14 06:06:53 UTC 2025 * bump targetSdk (#526) * Automated update of relay data - Sun Dec 21 06:06:56 UTC 2025 * Automated update of relay data - Sun Dec 28 06:07:12 UTC 2025 * Prevent quit notification from reappearing (#530) * shutdown sequence * Prevent quit notification from reappearing * Restrict force-finish broadcast * Cancel quit shutdown on relaunch * fix(wifi-aware): use bindSocket and scoped IPv6 instead of bindProcessToNetwork * Merge branch 'upstream/main' into fix/wifi-aware-socket-binding * Fix Wi-Fi Aware connectivity and UI integration post-merge - Replace bindProcessToNetwork with bindSocket for VPN compatibility. - Implement Scoped IPv6 address resolution (aware0) for mesh routing. - Bridge Wi-Fi Aware incoming messages to AppStateStore for UI visibility. - Fix syntax errors and variable name conflicts in Debug UI. * Enhance Wi-Fi Aware robustness and debug UI display - Clean up transport resources (sockets, server sockets, network callbacks) immediately on peer disconnection. - Implement resolveScopedAddress to show scoped IPv6 (e.g., %aware0) in Debug UI. - Fix Map type mismatch warning in ChatViewModel bridge. - Filter self-ID from peer cleanup tables to prevent recursive self-removal. * Share GossipSyncManager across transports to prevent redundant message synchronization - Registered BluetoothMeshService's GossipSyncManager as a singleton in MeshServiceHolder. - Modified WifiAwareMeshService to use the shared GossipSyncManager if available. - Added background cleanup for peer mappings on socket disconnection. - Fixed Kotlin type mismatch during nickname map merging. * Restore VPN acquisition logic and improve peer cleanup - Revert removal of NET_CAPABILITY_NOT_VPN to allow hardware handle acquisition while VPN is active. - Refactor handlePeerDisconnection to more reliably cleanup initial and routed IDs. - Switch cleanup logging to debug level to reduce log noise. * fix wifi aware routing --------- Co-authored-by: GitHub Action <action@github.com> Co-authored-by: aidenvalue <>
This commit is contained in:
co-authored by
GitHub Action
aidenvalue <>
parent
bd272bb5ea
commit
c12af45b6b
@@ -112,8 +112,9 @@ class MainActivity : OrientationAwareActivity() {
|
|||||||
// Ensure foreground service is running and get mesh instance from holder
|
// Ensure foreground service is running and get mesh instance from holder
|
||||||
try { com.bitchat.android.service.MeshForegroundService.start(applicationContext) } catch (_: Exception) { }
|
try { com.bitchat.android.service.MeshForegroundService.start(applicationContext) } catch (_: Exception) { }
|
||||||
meshService = com.bitchat.android.service.MeshServiceHolder.getOrCreate(applicationContext)
|
meshService = com.bitchat.android.service.MeshServiceHolder.getOrCreate(applicationContext)
|
||||||
// Expose BLE mesh to Wi‑Fi Aware controller for cross-transport relays
|
// Expose BLE mesh to Wi‑Fi Aware controller for cross-transport relays - DEPRECATED
|
||||||
try { com.bitchat.android.wifiaware.WifiAwareController.setBleMeshService(meshService) } catch (_: Exception) { }
|
// Bridging is now handled by TransportBridgeService automatically
|
||||||
|
|
||||||
bluetoothStatusManager = BluetoothStatusManager(
|
bluetoothStatusManager = BluetoothStatusManager(
|
||||||
activity = this,
|
activity = this,
|
||||||
context = this,
|
context = this,
|
||||||
@@ -180,7 +181,7 @@ class MainActivity : OrientationAwareActivity() {
|
|||||||
chatViewModel.didReceiveMessage(message)
|
chatViewModel.didReceiveMessage(message)
|
||||||
}
|
}
|
||||||
override fun didUpdatePeerList(peers: List<String>) {
|
override fun didUpdatePeerList(peers: List<String>) {
|
||||||
chatViewModel.didUpdatePeerList(peers)
|
chatViewModel.onWifiPeersUpdated(peers)
|
||||||
}
|
}
|
||||||
override fun didReceiveChannelLeave(channel: String, fromPeer: String) {
|
override fun didReceiveChannelLeave(channel: String, fromPeer: String) {
|
||||||
chatViewModel.didReceiveChannelLeave(channel, fromPeer)
|
chatViewModel.didReceiveChannelLeave(channel, fromPeer)
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import com.bitchat.android.protocol.SpecialRecipients
|
|||||||
import com.bitchat.android.model.RequestSyncPacket
|
import com.bitchat.android.model.RequestSyncPacket
|
||||||
import com.bitchat.android.sync.GossipSyncManager
|
import com.bitchat.android.sync.GossipSyncManager
|
||||||
import com.bitchat.android.util.toHexString
|
import com.bitchat.android.util.toHexString
|
||||||
|
import com.bitchat.android.service.TransportBridgeService
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.math.sign
|
import kotlin.math.sign
|
||||||
@@ -31,7 +32,7 @@ import kotlin.random.Random
|
|||||||
* - BluetoothConnectionManager: BLE connections and GATT operations
|
* - BluetoothConnectionManager: BLE connections and GATT operations
|
||||||
* - PacketProcessor: Incoming packet routing
|
* - PacketProcessor: Incoming packet routing
|
||||||
*/
|
*/
|
||||||
class BluetoothMeshService(private val context: Context) {
|
class BluetoothMeshService(private val context: Context) : TransportBridgeService.TransportLayer {
|
||||||
private val debugManager by lazy { try { com.bitchat.android.ui.debug.DebugSettingsManager.getInstance() } catch (e: Exception) { null } }
|
private val debugManager by lazy { try { com.bitchat.android.ui.debug.DebugSettingsManager.getInstance() } catch (e: Exception) { null } }
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -101,9 +102,15 @@ class BluetoothMeshService(private val context: Context) {
|
|||||||
// Wire sync manager delegate
|
// Wire sync manager delegate
|
||||||
gossipSyncManager.delegate = object : GossipSyncManager.Delegate {
|
gossipSyncManager.delegate = object : GossipSyncManager.Delegate {
|
||||||
override fun sendPacket(packet: BitchatPacket) {
|
override fun sendPacket(packet: BitchatPacket) {
|
||||||
connectionManager.broadcastPacket(RoutedPacket(packet))
|
dispatchGlobal(RoutedPacket(packet))
|
||||||
}
|
}
|
||||||
override fun sendPacketToPeer(peerID: String, packet: BitchatPacket) {
|
override fun sendPacketToPeer(peerID: String, packet: BitchatPacket) {
|
||||||
|
// Point-to-point optimization if possible, but for bridge safety
|
||||||
|
// we might want to consider dispatchGlobal if peer is on another transport.
|
||||||
|
// However, sendPacketToPeer in connectionManager is BLE-specific unicast.
|
||||||
|
// If peer is on Wi-Fi, this won't reach.
|
||||||
|
// For now, let's keep unicast as-is (it's mostly for sync)
|
||||||
|
// and assume routing handles the rest via broadcasts if needed.
|
||||||
connectionManager.sendPacketToPeer(peerID, packet)
|
connectionManager.sendPacketToPeer(peerID, packet)
|
||||||
}
|
}
|
||||||
override fun signPacketForBroadcast(packet: BitchatPacket): BitchatPacket {
|
override fun signPacketForBroadcast(packet: BitchatPacket): BitchatPacket {
|
||||||
@@ -111,6 +118,26 @@ class BluetoothMeshService(private val context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Log.d(TAG, "Delegates set up; GossipSyncManager initialized")
|
Log.d(TAG, "Delegates set up; GossipSyncManager initialized")
|
||||||
|
|
||||||
|
// Register with cross-layer transport bridge
|
||||||
|
TransportBridgeService.register("BLE", this)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TransportLayer implementation
|
||||||
|
override fun send(packet: RoutedPacket) {
|
||||||
|
// Received from bridge (e.g. Wi-Fi) -> Send via BLE
|
||||||
|
// Direct injection prevents routing loops (bridge handles source check)
|
||||||
|
connectionManager.broadcastPacket(packet)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* unified dispatch: Send to local BLE and bridge to other transports
|
||||||
|
*/
|
||||||
|
private fun dispatchGlobal(routed: RoutedPacket) {
|
||||||
|
// 1. Send to local BLE transport
|
||||||
|
connectionManager.broadcastPacket(routed)
|
||||||
|
// 2. Bridge to other transports (e.g. Wi-Fi)
|
||||||
|
TransportBridgeService.broadcast("BLE", routed)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -164,8 +191,6 @@ class BluetoothMeshService(private val context: Context) {
|
|||||||
// PeerManager delegates to main mesh service delegate
|
// PeerManager delegates to main mesh service delegate
|
||||||
peerManager.delegate = object : PeerManagerDelegate {
|
peerManager.delegate = object : PeerManagerDelegate {
|
||||||
override fun onPeerListUpdated(peerIDs: List<String>) {
|
override fun onPeerListUpdated(peerIDs: List<String>) {
|
||||||
// Update process-wide state first
|
|
||||||
try { com.bitchat.android.services.AppStateStore.setPeers(peerIDs) } catch (_: Exception) { }
|
|
||||||
// Then notify UI delegate if attached
|
// Then notify UI delegate if attached
|
||||||
delegate?.didUpdatePeerList(peerIDs)
|
delegate?.didUpdatePeerList(peerIDs)
|
||||||
}
|
}
|
||||||
@@ -208,7 +233,7 @@ class BluetoothMeshService(private val context: Context) {
|
|||||||
)
|
)
|
||||||
// Sign the handshake response
|
// Sign the handshake response
|
||||||
val signedPacket = signPacketBeforeBroadcast(responsePacket)
|
val signedPacket = signPacketBeforeBroadcast(responsePacket)
|
||||||
connectionManager.broadcastPacket(RoutedPacket(signedPacket))
|
dispatchGlobal(RoutedPacket(signedPacket))
|
||||||
Log.d(TAG, "Sent Noise handshake response to $peerID (${response.size} bytes)")
|
Log.d(TAG, "Sent Noise handshake response to $peerID (${response.size} bytes)")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,7 +253,7 @@ class BluetoothMeshService(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun sendPacket(packet: BitchatPacket) {
|
override fun sendPacket(packet: BitchatPacket) {
|
||||||
connectionManager.broadcastPacket(RoutedPacket(packet))
|
dispatchGlobal(RoutedPacket(packet))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,11 +297,11 @@ class BluetoothMeshService(private val context: Context) {
|
|||||||
// Sign the packet before broadcasting
|
// Sign the packet before broadcasting
|
||||||
val signedPacket = signPacketBeforeBroadcast(packet)
|
val signedPacket = signPacketBeforeBroadcast(packet)
|
||||||
val routed = RoutedPacket(signedPacket)
|
val routed = RoutedPacket(signedPacket)
|
||||||
connectionManager.broadcastPacket(routed)
|
dispatchGlobal(routed)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun relayPacket(routed: RoutedPacket) {
|
override fun relayPacket(routed: RoutedPacket) {
|
||||||
connectionManager.broadcastPacket(routed)
|
dispatchGlobal(routed)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getBroadcastRecipient(): ByteArray {
|
override fun getBroadcastRecipient(): ByteArray {
|
||||||
@@ -323,7 +348,7 @@ class BluetoothMeshService(private val context: Context) {
|
|||||||
|
|
||||||
// Sign the handshake packet before broadcasting
|
// Sign the handshake packet before broadcasting
|
||||||
val signedPacket = signPacketBeforeBroadcast(packet)
|
val signedPacket = signPacketBeforeBroadcast(packet)
|
||||||
connectionManager.broadcastPacket(RoutedPacket(signedPacket))
|
dispatchGlobal(RoutedPacket(signedPacket))
|
||||||
Log.d(TAG, "Initiated Noise handshake with $peerID (${handshakeData.size} bytes)")
|
Log.d(TAG, "Initiated Noise handshake with $peerID (${handshakeData.size} bytes)")
|
||||||
} else {
|
} else {
|
||||||
Log.w(TAG, "Failed to generate Noise handshake data for $peerID")
|
Log.w(TAG, "Failed to generate Noise handshake data for $peerID")
|
||||||
@@ -515,7 +540,7 @@ class BluetoothMeshService(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun relayPacket(routed: RoutedPacket) {
|
override fun relayPacket(routed: RoutedPacket) {
|
||||||
connectionManager.broadcastPacket(routed)
|
dispatchGlobal(routed)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handleRequestSync(routed: RoutedPacket) {
|
override fun handleRequestSync(routed: RoutedPacket) {
|
||||||
@@ -632,6 +657,9 @@ class BluetoothMeshService(private val context: Context) {
|
|||||||
|
|
||||||
Log.i(TAG, "Stopping Bluetooth mesh service")
|
Log.i(TAG, "Stopping Bluetooth mesh service")
|
||||||
isActive = false
|
isActive = false
|
||||||
|
|
||||||
|
// Unregister from bridge
|
||||||
|
TransportBridgeService.unregister("BLE")
|
||||||
|
|
||||||
// Send leave announcement
|
// Send leave announcement
|
||||||
sendLeaveAnnouncement()
|
sendLeaveAnnouncement()
|
||||||
@@ -691,7 +719,7 @@ class BluetoothMeshService(private val context: Context) {
|
|||||||
|
|
||||||
// Sign the packet before broadcasting
|
// Sign the packet before broadcasting
|
||||||
val signedPacket = signPacketBeforeBroadcast(packet)
|
val signedPacket = signPacketBeforeBroadcast(packet)
|
||||||
connectionManager.broadcastPacket(RoutedPacket(signedPacket))
|
dispatchGlobal(RoutedPacket(signedPacket))
|
||||||
// Track our own broadcast message for sync
|
// Track our own broadcast message for sync
|
||||||
try { gossipSyncManager.onPublicPacketSeen(signedPacket) } catch (_: Exception) { }
|
try { gossipSyncManager.onPublicPacketSeen(signedPacket) } catch (_: Exception) { }
|
||||||
}
|
}
|
||||||
@@ -723,7 +751,7 @@ class BluetoothMeshService(private val context: Context) {
|
|||||||
val signed = signPacketBeforeBroadcast(packet)
|
val signed = signPacketBeforeBroadcast(packet)
|
||||||
// Use a stable transferId based on the file TLV payload for progress tracking
|
// Use a stable transferId based on the file TLV payload for progress tracking
|
||||||
val transferId = sha256Hex(payload)
|
val transferId = sha256Hex(payload)
|
||||||
connectionManager.broadcastPacket(RoutedPacket(signed, transferId = transferId))
|
dispatchGlobal(RoutedPacket(signed, transferId = transferId))
|
||||||
try { gossipSyncManager.onPublicPacketSeen(signed) } catch (_: Exception) { }
|
try { gossipSyncManager.onPublicPacketSeen(signed) } catch (_: Exception) { }
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -781,7 +809,7 @@ class BluetoothMeshService(private val context: Context) {
|
|||||||
val signed = signPacketBeforeBroadcast(packet)
|
val signed = signPacketBeforeBroadcast(packet)
|
||||||
// Use a stable transferId based on the unencrypted file TLV payload for progress tracking
|
// Use a stable transferId based on the unencrypted file TLV payload for progress tracking
|
||||||
val transferId = sha256Hex(filePayload)
|
val transferId = sha256Hex(filePayload)
|
||||||
connectionManager.broadcastPacket(RoutedPacket(signed, transferId = transferId))
|
dispatchGlobal(RoutedPacket(signed, transferId = transferId))
|
||||||
Log.d(TAG, "✅ Sent encrypted file to $recipientPeerID")
|
Log.d(TAG, "✅ Sent encrypted file to $recipientPeerID")
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -861,7 +889,7 @@ class BluetoothMeshService(private val context: Context) {
|
|||||||
|
|
||||||
// Sign the packet before broadcasting
|
// Sign the packet before broadcasting
|
||||||
val signedPacket = signPacketBeforeBroadcast(packet)
|
val signedPacket = signPacketBeforeBroadcast(packet)
|
||||||
connectionManager.broadcastPacket(RoutedPacket(signedPacket))
|
dispatchGlobal(RoutedPacket(signedPacket))
|
||||||
Log.d(TAG, "📤 Sent encrypted private message to $recipientPeerID (${encrypted.size} bytes)")
|
Log.d(TAG, "📤 Sent encrypted private message to $recipientPeerID (${encrypted.size} bytes)")
|
||||||
|
|
||||||
// FIXED: Don't send didReceiveMessage for our own sent messages
|
// FIXED: Don't send didReceiveMessage for our own sent messages
|
||||||
@@ -932,7 +960,7 @@ class BluetoothMeshService(private val context: Context) {
|
|||||||
|
|
||||||
// Sign the packet before broadcasting
|
// Sign the packet before broadcasting
|
||||||
val signedPacket = signPacketBeforeBroadcast(packet)
|
val signedPacket = signPacketBeforeBroadcast(packet)
|
||||||
connectionManager.broadcastPacket(RoutedPacket(signedPacket))
|
dispatchGlobal(RoutedPacket(signedPacket))
|
||||||
Log.d(TAG, "📤 Sent read receipt to $recipientPeerID for message $messageID")
|
Log.d(TAG, "📤 Sent read receipt to $recipientPeerID for message $messageID")
|
||||||
|
|
||||||
// Persist as read after successful send
|
// Persist as read after successful send
|
||||||
@@ -986,7 +1014,7 @@ class BluetoothMeshService(private val context: Context) {
|
|||||||
announcePacket.copy(signature = signature)
|
announcePacket.copy(signature = signature)
|
||||||
} ?: announcePacket
|
} ?: announcePacket
|
||||||
|
|
||||||
connectionManager.broadcastPacket(RoutedPacket(signedPacket))
|
dispatchGlobal(RoutedPacket(signedPacket))
|
||||||
Log.d(TAG, "Sent iOS-compatible signed TLV announce (${tlvPayload.size} bytes)")
|
Log.d(TAG, "Sent iOS-compatible signed TLV announce (${tlvPayload.size} bytes)")
|
||||||
// Track announce for sync
|
// Track announce for sync
|
||||||
try { gossipSyncManager.onPublicPacketSeen(signedPacket) } catch (_: Exception) { }
|
try { gossipSyncManager.onPublicPacketSeen(signedPacket) } catch (_: Exception) { }
|
||||||
@@ -1035,7 +1063,7 @@ class BluetoothMeshService(private val context: Context) {
|
|||||||
packet.copy(signature = signature)
|
packet.copy(signature = signature)
|
||||||
} ?: packet
|
} ?: packet
|
||||||
|
|
||||||
connectionManager.broadcastPacket(RoutedPacket(signedPacket))
|
dispatchGlobal(RoutedPacket(signedPacket))
|
||||||
peerManager.markPeerAsAnnouncedTo(peerID)
|
peerManager.markPeerAsAnnouncedTo(peerID)
|
||||||
Log.d(TAG, "Sent iOS-compatible signed TLV peer announce to $peerID (${tlvPayload.size} bytes)")
|
Log.d(TAG, "Sent iOS-compatible signed TLV peer announce to $peerID (${tlvPayload.size} bytes)")
|
||||||
|
|
||||||
@@ -1056,7 +1084,7 @@ class BluetoothMeshService(private val context: Context) {
|
|||||||
|
|
||||||
// Sign the packet before broadcasting
|
// Sign the packet before broadcasting
|
||||||
val signedPacket = signPacketBeforeBroadcast(packet)
|
val signedPacket = signPacketBeforeBroadcast(packet)
|
||||||
connectionManager.broadcastPacket(RoutedPacket(signedPacket))
|
dispatchGlobal(RoutedPacket(signedPacket))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package com.bitchat.android.service
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import com.bitchat.android.model.RoutedPacket
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Central bridge for routing packets between different transport layers
|
||||||
|
* (e.g., Bluetooth LE <-> Wi-Fi Aware).
|
||||||
|
*
|
||||||
|
* Allows a packet received on one transport to be seamlessly relayed
|
||||||
|
* to all other active transports, effectively bridging separate meshes.
|
||||||
|
*/
|
||||||
|
object TransportBridgeService {
|
||||||
|
private const val TAG = "TransportBridgeService"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface that any transport layer (BLE, WiFi, Tor, etc.) must implement
|
||||||
|
* to receive bridged packets.
|
||||||
|
*/
|
||||||
|
interface TransportLayer {
|
||||||
|
/**
|
||||||
|
* Send a packet out via this transport.
|
||||||
|
*/
|
||||||
|
fun send(packet: RoutedPacket)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val transports = ConcurrentHashMap<String, TransportLayer>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a transport layer to receive bridged packets.
|
||||||
|
* @param id Unique identifier (e.g., "BLE", "WIFI")
|
||||||
|
* @param layer The transport implementation
|
||||||
|
*/
|
||||||
|
fun register(id: String, layer: TransportLayer) {
|
||||||
|
Log.i(TAG, "Registering transport layer: $id")
|
||||||
|
transports[id] = layer
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unregister a transport layer.
|
||||||
|
*/
|
||||||
|
fun unregister(id: String) {
|
||||||
|
Log.i(TAG, "Unregistering transport layer: $id")
|
||||||
|
transports.remove(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Broadcast a packet from a specific source transport to ALL other registered transports.
|
||||||
|
*
|
||||||
|
* @param sourceId The ID of the transport initiating the broadcast (e.g., "BLE").
|
||||||
|
* The packet will NOT be sent back to this source.
|
||||||
|
* @param packet The packet to bridge.
|
||||||
|
*/
|
||||||
|
fun broadcast(sourceId: String, packet: RoutedPacket) {
|
||||||
|
val targets = transports.filterKeys { it != sourceId }
|
||||||
|
if (targets.isEmpty()) return
|
||||||
|
|
||||||
|
// Log.v(TAG, "Bridging packet type ${packet.packet.type} from $sourceId to ${targets.keys}")
|
||||||
|
|
||||||
|
targets.forEach { (id, layer) ->
|
||||||
|
try {
|
||||||
|
layer.send(packet)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to bridge packet to $id: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -744,6 +744,10 @@ class ChatViewModel(
|
|||||||
override fun didUpdatePeerList(peers: List<String>) {
|
override fun didUpdatePeerList(peers: List<String>) {
|
||||||
meshDelegateHandler.didUpdatePeerList(peers)
|
meshDelegateHandler.didUpdatePeerList(peers)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun onWifiPeersUpdated(peers: List<String>) {
|
||||||
|
meshDelegateHandler.onWifiPeersUpdated(peers)
|
||||||
|
}
|
||||||
|
|
||||||
override fun didReceiveChannelLeave(channel: String, fromPeer: String) {
|
override fun didReceiveChannelLeave(channel: String, fromPeer: String) {
|
||||||
meshDelegateHandler.didReceiveChannelLeave(channel, fromPeer)
|
meshDelegateHandler.didReceiveChannelLeave(channel, fromPeer)
|
||||||
|
|||||||
@@ -94,104 +94,123 @@ class MeshDelegateHandler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var blePeers: Set<String> = emptySet()
|
||||||
|
private var wifiPeers: Set<String> = emptySet()
|
||||||
|
|
||||||
override fun didUpdatePeerList(peers: List<String>) {
|
override fun didUpdatePeerList(peers: List<String>) {
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
// Merge peers from multiple transports to avoid flapping
|
blePeers = peers.toSet()
|
||||||
val current = state.getConnectedPeersValue().toMutableSet()
|
processPeerUpdate()
|
||||||
current.addAll(peers)
|
}
|
||||||
state.setConnectedPeers(current.toList())
|
}
|
||||||
state.setIsConnected(peers.isNotEmpty())
|
|
||||||
notificationManager.showActiveUserNotification(peers)
|
|
||||||
// Flush router outbox for any peers that just connected (and their noiseHex aliases)
|
|
||||||
runCatching { com.bitchat.android.services.MessageRouter.tryGetInstance()?.onPeersUpdated(peers) }
|
|
||||||
|
|
||||||
// Clean up channel members who disconnected
|
fun onWifiPeersUpdated(peers: List<String>) {
|
||||||
channelManager.cleanupDisconnectedMembers(peers, getMyPeerID())
|
coroutineScope.launch {
|
||||||
|
wifiPeers = peers.toSet()
|
||||||
|
processPeerUpdate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Handle chat view migration based on current selection and new peer list
|
private suspend fun processPeerUpdate() {
|
||||||
state.getSelectedPrivateChatPeerValue()?.let { currentPeer ->
|
// Merge peers from multiple transports
|
||||||
val isNostrAlias = currentPeer.startsWith("nostr_")
|
val mergedPeers = (blePeers + wifiPeers).toList()
|
||||||
val isNoiseHex = currentPeer.length == 64 && currentPeer.matches(Regex("^[0-9a-fA-F]+$"))
|
|
||||||
val isMeshEphemeral = currentPeer.length == 16 && currentPeer.matches(Regex("^[0-9a-fA-F]+$"))
|
// Update process-wide state as source of truth
|
||||||
|
try { com.bitchat.android.services.AppStateStore.setPeers(mergedPeers) } catch (_: Exception) { }
|
||||||
|
|
||||||
if (isNostrAlias || isNoiseHex) {
|
state.setConnectedPeers(mergedPeers)
|
||||||
// Reverse case: Nostr/offline chat is open, and peer may have come online on mesh.
|
state.setIsConnected(mergedPeers.isNotEmpty())
|
||||||
// Resolve canonical target (prefer connected mesh peer if available)
|
notificationManager.showActiveUserNotification(mergedPeers)
|
||||||
val canonical = com.bitchat.android.services.ConversationAliasResolver.resolveCanonicalPeerID(
|
|
||||||
selectedPeerID = currentPeer,
|
// Flush router outbox for any peers that just connected (and their noiseHex aliases)
|
||||||
connectedPeers = peers,
|
runCatching { com.bitchat.android.services.MessageRouter.tryGetInstance()?.onPeersUpdated(mergedPeers) }
|
||||||
meshNoiseKeyForPeer = { pid -> getPeerInfo(pid)?.noisePublicKey },
|
|
||||||
meshHasPeer = { pid -> peers.contains(pid) },
|
// Clean up channel members who disconnected
|
||||||
nostrPubHexForAlias = { alias ->
|
channelManager.cleanupDisconnectedMembers(mergedPeers, getMyPeerID())
|
||||||
// Use GeohashAliasRegistry for geohash aliases, but for mesh favorites, derive from favorites mapping
|
|
||||||
if (com.bitchat.android.nostr.GeohashAliasRegistry.contains(alias)) {
|
// Handle chat view migration based on current selection and new peer list
|
||||||
com.bitchat.android.nostr.GeohashAliasRegistry.get(alias)
|
state.getSelectedPrivateChatPeerValue()?.let { currentPeer ->
|
||||||
} else {
|
val isNostrAlias = currentPeer.startsWith("nostr_")
|
||||||
// Best-effort: derive pub hex from favorites mapping for mesh nostr_ aliases
|
val isNoiseHex = currentPeer.length == 64 && currentPeer.matches(Regex("^[0-9a-fA-F]+$"))
|
||||||
val prefix = alias.removePrefix("nostr_")
|
val isMeshEphemeral = currentPeer.length == 16 && currentPeer.matches(Regex("^[0-9a-fA-F]+$"))
|
||||||
val favs = try { com.bitchat.android.favorites.FavoritesPersistenceService.shared.getOurFavorites() } catch (_: Exception) { emptyList() }
|
|
||||||
favs.firstNotNullOfOrNull { rel ->
|
if (isNostrAlias || isNoiseHex) {
|
||||||
rel.peerNostrPublicKey?.let { s ->
|
// Reverse case: Nostr/offline chat is open, and peer may have come online on mesh.
|
||||||
runCatching { com.bitchat.android.nostr.Bech32.decode(s) }.getOrNull()?.let { dec ->
|
// Resolve canonical target (prefer connected mesh peer if available)
|
||||||
if (dec.first == "npub") dec.second.joinToString("") { b -> "%02x".format(b) } else null
|
val canonical = com.bitchat.android.services.ConversationAliasResolver.resolveCanonicalPeerID(
|
||||||
}
|
selectedPeerID = currentPeer,
|
||||||
|
connectedPeers = mergedPeers,
|
||||||
|
meshNoiseKeyForPeer = { pid -> getPeerInfo(pid)?.noisePublicKey },
|
||||||
|
meshHasPeer = { pid -> mergedPeers.contains(pid) },
|
||||||
|
nostrPubHexForAlias = { alias ->
|
||||||
|
// Use GeohashAliasRegistry for geohash aliases, but for mesh favorites, derive from favorites mapping
|
||||||
|
if (com.bitchat.android.nostr.GeohashAliasRegistry.contains(alias)) {
|
||||||
|
com.bitchat.android.nostr.GeohashAliasRegistry.get(alias)
|
||||||
|
} else {
|
||||||
|
// Best-effort: derive pub hex from favorites mapping for mesh nostr_ aliases
|
||||||
|
val prefix = alias.removePrefix("nostr_")
|
||||||
|
val favs = try { com.bitchat.android.favorites.FavoritesPersistenceService.shared.getOurFavorites() } catch (_: Exception) { emptyList() }
|
||||||
|
favs.firstNotNullOfOrNull { rel ->
|
||||||
|
rel.peerNostrPublicKey?.let { s ->
|
||||||
|
runCatching { com.bitchat.android.nostr.Bech32.decode(s) }.getOrNull()?.let { dec ->
|
||||||
|
if (dec.first == "npub") dec.second.joinToString("") { b -> "%02x".format(b) } else null
|
||||||
}
|
}
|
||||||
}?.takeIf { it.startsWith(prefix, ignoreCase = true) }
|
}
|
||||||
}
|
}?.takeIf { it.startsWith(prefix, ignoreCase = true) }
|
||||||
},
|
|
||||||
findNoiseKeyForNostr = { key -> com.bitchat.android.favorites.FavoritesPersistenceService.shared.findNoiseKey(key) }
|
|
||||||
)
|
|
||||||
if (canonical != currentPeer) {
|
|
||||||
// Merge conversations and switch selection to the live mesh peer (or noiseHex)
|
|
||||||
com.bitchat.android.services.ConversationAliasResolver.unifyChatsIntoPeer(state, canonical, listOf(currentPeer))
|
|
||||||
state.setSelectedPrivateChatPeer(canonical)
|
|
||||||
}
|
|
||||||
} else if (isMeshEphemeral && !peers.contains(currentPeer)) {
|
|
||||||
// Forward case: Mesh chat lost connection. If mutual favorite exists, migrate to Nostr (noiseHex)
|
|
||||||
val favoriteRel = try {
|
|
||||||
val info = getPeerInfo(currentPeer)
|
|
||||||
val noiseKey = info?.noisePublicKey
|
|
||||||
if (noiseKey != null) {
|
|
||||||
com.bitchat.android.favorites.FavoritesPersistenceService.shared.getFavoriteStatus(noiseKey)
|
|
||||||
} else null
|
|
||||||
} catch (_: Exception) { null }
|
|
||||||
|
|
||||||
if (favoriteRel?.isMutual == true) {
|
|
||||||
val noiseHex = favoriteRel.peerNoisePublicKey.joinToString("") { b -> "%02x".format(b) }
|
|
||||||
if (noiseHex != currentPeer) {
|
|
||||||
com.bitchat.android.services.ConversationAliasResolver.unifyChatsIntoPeer(
|
|
||||||
state = state,
|
|
||||||
targetPeerID = noiseHex,
|
|
||||||
keysToMerge = listOf(currentPeer)
|
|
||||||
)
|
|
||||||
state.setSelectedPrivateChatPeer(noiseHex)
|
|
||||||
}
|
}
|
||||||
} else {
|
},
|
||||||
privateChatManager.cleanupDisconnectedPeer(currentPeer)
|
findNoiseKeyForNostr = { key -> com.bitchat.android.favorites.FavoritesPersistenceService.shared.findNoiseKey(key) }
|
||||||
|
)
|
||||||
|
if (canonical != currentPeer) {
|
||||||
|
// Merge conversations and switch selection to the live mesh peer (or noiseHex)
|
||||||
|
com.bitchat.android.services.ConversationAliasResolver.unifyChatsIntoPeer(state, canonical, listOf(currentPeer))
|
||||||
|
state.setSelectedPrivateChatPeer(canonical)
|
||||||
|
}
|
||||||
|
} else if (isMeshEphemeral && !mergedPeers.contains(currentPeer)) {
|
||||||
|
// Forward case: Mesh chat lost connection. If mutual favorite exists, migrate to Nostr (noiseHex)
|
||||||
|
val favoriteRel = try {
|
||||||
|
val info = getPeerInfo(currentPeer)
|
||||||
|
val noiseKey = info?.noisePublicKey
|
||||||
|
if (noiseKey != null) {
|
||||||
|
com.bitchat.android.favorites.FavoritesPersistenceService.shared.getFavoriteStatus(noiseKey)
|
||||||
|
} else null
|
||||||
|
} catch (_: Exception) { null }
|
||||||
|
|
||||||
|
if (favoriteRel?.isMutual == true) {
|
||||||
|
val noiseHex = favoriteRel.peerNoisePublicKey.joinToString("") { b -> "%02x".format(b) }
|
||||||
|
if (noiseHex != currentPeer) {
|
||||||
|
com.bitchat.android.services.ConversationAliasResolver.unifyChatsIntoPeer(
|
||||||
|
state = state,
|
||||||
|
targetPeerID = noiseHex,
|
||||||
|
keysToMerge = listOf(currentPeer)
|
||||||
|
)
|
||||||
|
state.setSelectedPrivateChatPeer(noiseHex)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
privateChatManager.cleanupDisconnectedPeer(currentPeer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Global unification: for each connected peer, merge any offline/stable conversations
|
// Global unification: for each connected peer, merge any offline/stable conversations
|
||||||
// (noiseHex or nostr_<pub16>) into the connected peer's chat so there is only one chat per identity.
|
// (noiseHex or nostr_<pub16>) into the connected peer's chat so there is only one chat per identity.
|
||||||
peers.forEach { pid ->
|
mergedPeers.forEach { pid ->
|
||||||
try {
|
try {
|
||||||
val info = getPeerInfo(pid)
|
val info = getPeerInfo(pid)
|
||||||
val noiseKey = info?.noisePublicKey ?: return@forEach
|
val noiseKey = info?.noisePublicKey ?: return@forEach
|
||||||
val noiseHex = noiseKey.joinToString("") { b -> "%02x".format(b) }
|
val noiseHex = noiseKey.joinToString("") { b -> "%02x".format(b) }
|
||||||
|
|
||||||
// Derive temp nostr key from favorites npub
|
// Derive temp nostr key from favorites npub
|
||||||
val npub = com.bitchat.android.favorites.FavoritesPersistenceService.shared.findNostrPubkey(noiseKey)
|
val npub = com.bitchat.android.favorites.FavoritesPersistenceService.shared.findNostrPubkey(noiseKey)
|
||||||
val tempNostrKey: String? = try {
|
val tempNostrKey: String? = try {
|
||||||
if (npub != null) {
|
if (npub != null) {
|
||||||
val (hrp, data) = com.bitchat.android.nostr.Bech32.decode(npub)
|
val (hrp, data) = com.bitchat.android.nostr.Bech32.decode(npub)
|
||||||
if (hrp == "npub") "nostr_${data.joinToString("") { b -> "%02x".format(b) }.take(16)}" else null
|
if (hrp == "npub") "nostr_${data.joinToString("") { b -> "%02x".format(b) }.take(16)}" else null
|
||||||
} else null
|
} else null
|
||||||
} catch (_: Exception) { null }
|
} catch (_: Exception) { null }
|
||||||
|
|
||||||
unifyChatsIntoPeer(pid, listOfNotNull(noiseHex, tempNostrKey))
|
unifyChatsIntoPeer(pid, listOfNotNull(noiseHex, tempNostrKey))
|
||||||
} catch (_: Exception) { }
|
} catch (_: Exception) { }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -108,9 +108,4 @@ object WifiAwareController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getService(): WifiAwareMeshService? = service
|
fun getService(): WifiAwareMeshService? = service
|
||||||
|
|
||||||
// Optional bridge to BLE mesh for cross-transport relaying
|
|
||||||
@Volatile private var bleMesh: com.bitchat.android.mesh.BluetoothMeshService? = null
|
|
||||||
fun setBleMeshService(svc: com.bitchat.android.mesh.BluetoothMeshService) { bleMesh = svc }
|
|
||||||
fun getBleMeshService(): com.bitchat.android.mesh.BluetoothMeshService? = bleMesh
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import androidx.annotation.RequiresPermission
|
|||||||
import com.bitchat.android.crypto.EncryptionService
|
import com.bitchat.android.crypto.EncryptionService
|
||||||
import com.bitchat.android.model.*
|
import com.bitchat.android.model.*
|
||||||
import com.bitchat.android.protocol.*
|
import com.bitchat.android.protocol.*
|
||||||
|
import com.bitchat.android.service.TransportBridgeService
|
||||||
import com.bitchat.android.sync.GossipSyncManager
|
import com.bitchat.android.sync.GossipSyncManager
|
||||||
import com.bitchat.android.util.toHexString
|
import com.bitchat.android.util.toHexString
|
||||||
// Mesh-layer components are reused from the existing Bluetooth stack
|
// Mesh-layer components are reused from the existing Bluetooth stack
|
||||||
@@ -51,7 +52,7 @@ import java.util.concurrent.Executors
|
|||||||
* - MessageHandler: Message type processing and relay logic
|
* - MessageHandler: Message type processing and relay logic
|
||||||
* - PacketProcessor: Incoming packet routing
|
* - PacketProcessor: Incoming packet routing
|
||||||
*/
|
*/
|
||||||
class WifiAwareMeshService(private val context: Context) {
|
class WifiAwareMeshService(private val context: Context) : TransportBridgeService.TransportLayer {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "WifiAwareMeshService"
|
private const val TAG = "WifiAwareMeshService"
|
||||||
@@ -95,6 +96,7 @@ class WifiAwareMeshService(private val context: Context) {
|
|||||||
private val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
private val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||||
private val handleToPeerId = ConcurrentHashMap<PeerHandle, String>() // discovery mapping
|
private val handleToPeerId = ConcurrentHashMap<PeerHandle, String>() // discovery mapping
|
||||||
private val discoveredTimestamps = ConcurrentHashMap<String, Long>() // peerID -> last seen time
|
private val discoveredTimestamps = ConcurrentHashMap<String, Long>() // peerID -> last seen time
|
||||||
|
private val connectionAttempts = ConcurrentHashMap<String, Long>() // peerID -> last connection attempt time
|
||||||
|
|
||||||
// Timestamp dedupe
|
// Timestamp dedupe
|
||||||
private val lastTimestamps = ConcurrentHashMap<String, ULong>()
|
private val lastTimestamps = ConcurrentHashMap<String, ULong>()
|
||||||
@@ -125,7 +127,7 @@ class WifiAwareMeshService(private val context: Context) {
|
|||||||
this@WifiAwareMeshService.sendPacketToPeer(peerID, packet)
|
this@WifiAwareMeshService.sendPacketToPeer(peerID, packet)
|
||||||
}
|
}
|
||||||
override fun sendPacket(packet: BitchatPacket) {
|
override fun sendPacket(packet: BitchatPacket) {
|
||||||
broadcastPacket(RoutedPacket(packet))
|
dispatchGlobal(RoutedPacket(packet))
|
||||||
}
|
}
|
||||||
override fun signPacketForBroadcast(packet: BitchatPacket): BitchatPacket {
|
override fun signPacketForBroadcast(packet: BitchatPacket): BitchatPacket {
|
||||||
return signPacketBeforeBroadcast(packet)
|
return signPacketBeforeBroadcast(packet)
|
||||||
@@ -174,6 +176,23 @@ class WifiAwareMeshService(private val context: Context) {
|
|||||||
Log.i(TAG, "TX: broadcast via Wi-Fi Aware to $sent peers (bytes=${bytes.size})")
|
Log.i(TAG, "TX: broadcast via Wi-Fi Aware to $sent peers (bytes=${bytes.size})")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TransportLayer implementation
|
||||||
|
override fun send(packet: RoutedPacket) {
|
||||||
|
// Received from bridge (e.g. BLE) -> Send via Wi-Fi
|
||||||
|
// Direct injection prevents routing loops (bridge handles source check)
|
||||||
|
broadcastPacket(packet)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* unified dispatch: Send to local Wi-Fi and bridge to other transports
|
||||||
|
*/
|
||||||
|
private fun dispatchGlobal(routed: RoutedPacket) {
|
||||||
|
// 1. Send to local Wi-Fi transport
|
||||||
|
broadcastPacket(routed)
|
||||||
|
// 2. Bridge to other transports (e.g. BLE)
|
||||||
|
TransportBridgeService.broadcast("WIFI", routed)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Broadcasts routed packet to currently connected peers.
|
* Broadcasts routed packet to currently connected peers.
|
||||||
*/
|
*/
|
||||||
@@ -244,7 +263,7 @@ class WifiAwareMeshService(private val context: Context) {
|
|||||||
payload = response,
|
payload = response,
|
||||||
ttl = MAX_TTL
|
ttl = MAX_TTL
|
||||||
)
|
)
|
||||||
broadcastPacket(RoutedPacket(signPacketBeforeBroadcast(packet)))
|
dispatchGlobal(RoutedPacket(signPacketBeforeBroadcast(packet)))
|
||||||
}
|
}
|
||||||
override fun getPeerInfo(peerID: String): PeerInfo? {
|
override fun getPeerInfo(peerID: String): PeerInfo? {
|
||||||
return peerManager.getPeerInfo(peerID)
|
return peerManager.getPeerInfo(peerID)
|
||||||
@@ -255,7 +274,7 @@ class WifiAwareMeshService(private val context: Context) {
|
|||||||
override fun isFavorite(peerID: String) = delegate?.isFavorite(peerID) ?: false
|
override fun isFavorite(peerID: String) = delegate?.isFavorite(peerID) ?: false
|
||||||
override fun isPeerOnline(peerID: String) = peerManager.isPeerActive(peerID)
|
override fun isPeerOnline(peerID: String) = peerManager.isPeerActive(peerID)
|
||||||
override fun sendPacket(packet: BitchatPacket) {
|
override fun sendPacket(packet: BitchatPacket) {
|
||||||
broadcastPacket(RoutedPacket(packet))
|
dispatchGlobal(RoutedPacket(packet))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -280,9 +299,9 @@ class WifiAwareMeshService(private val context: Context) {
|
|||||||
): Boolean = peerManager.updatePeerInfo(peerID, nickname, noisePublicKey, signingPublicKey, isVerified)
|
): Boolean = peerManager.updatePeerInfo(peerID, nickname, noisePublicKey, signingPublicKey, isVerified)
|
||||||
|
|
||||||
override fun sendPacket(packet: BitchatPacket) {
|
override fun sendPacket(packet: BitchatPacket) {
|
||||||
broadcastPacket(RoutedPacket(signPacketBeforeBroadcast(packet)))
|
dispatchGlobal(RoutedPacket(signPacketBeforeBroadcast(packet)))
|
||||||
}
|
}
|
||||||
override fun relayPacket(routed: RoutedPacket) { broadcastPacket(routed) }
|
override fun relayPacket(routed: RoutedPacket) { dispatchGlobal(routed) }
|
||||||
override fun getBroadcastRecipient() = SpecialRecipients.BROADCAST
|
override fun getBroadcastRecipient() = SpecialRecipients.BROADCAST
|
||||||
|
|
||||||
override fun verifySignature(packet: BitchatPacket, peerID: String) =
|
override fun verifySignature(packet: BitchatPacket, peerID: String) =
|
||||||
@@ -308,7 +327,7 @@ class WifiAwareMeshService(private val context: Context) {
|
|||||||
payload = hs,
|
payload = hs,
|
||||||
ttl = MAX_TTL
|
ttl = MAX_TTL
|
||||||
)
|
)
|
||||||
broadcastPacket(RoutedPacket(signPacketBeforeBroadcast(packet)))
|
dispatchGlobal(RoutedPacket(signPacketBeforeBroadcast(packet)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
override fun processNoiseHandshakeMessage(payload: ByteArray, peerID: String): ByteArray? =
|
override fun processNoiseHandshakeMessage(payload: ByteArray, peerID: String): ByteArray? =
|
||||||
@@ -390,7 +409,7 @@ class WifiAwareMeshService(private val context: Context) {
|
|||||||
|
|
||||||
override fun sendAnnouncementToPeer(peerID: String) = this@WifiAwareMeshService.sendAnnouncementToPeer(peerID)
|
override fun sendAnnouncementToPeer(peerID: String) = this@WifiAwareMeshService.sendAnnouncementToPeer(peerID)
|
||||||
override fun sendCachedMessages(peerID: String) = storeForwardManager.sendCachedMessages(peerID)
|
override fun sendCachedMessages(peerID: String) = storeForwardManager.sendCachedMessages(peerID)
|
||||||
override fun relayPacket(routed: RoutedPacket) = broadcastPacket(routed)
|
override fun relayPacket(routed: RoutedPacket) = dispatchGlobal(routed)
|
||||||
|
|
||||||
override fun handleRequestSync(routed: RoutedPacket) {
|
override fun handleRequestSync(routed: RoutedPacket) {
|
||||||
val fromPeer = routed.peerID ?: return
|
val fromPeer = routed.peerID ?: return
|
||||||
@@ -512,7 +531,11 @@ class WifiAwareMeshService(private val context: Context) {
|
|||||||
}, Handler(Looper.getMainLooper()))
|
}, Handler(Looper.getMainLooper()))
|
||||||
|
|
||||||
sendPeriodicBroadcastAnnounce()
|
sendPeriodicBroadcastAnnounce()
|
||||||
|
startPeriodicConnectionMaintenance()
|
||||||
gossipSyncManager.start()
|
gossipSyncManager.start()
|
||||||
|
|
||||||
|
// Register with cross-layer transport bridge
|
||||||
|
TransportBridgeService.register("WIFI", this)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -523,6 +546,9 @@ class WifiAwareMeshService(private val context: Context) {
|
|||||||
isActive = false
|
isActive = false
|
||||||
Log.i(TAG, "Stopping Wi-Fi Aware mesh")
|
Log.i(TAG, "Stopping Wi-Fi Aware mesh")
|
||||||
|
|
||||||
|
// Unregister from bridge
|
||||||
|
TransportBridgeService.unregister("WIFI")
|
||||||
|
|
||||||
sendLeaveAnnouncement()
|
sendLeaveAnnouncement()
|
||||||
|
|
||||||
serviceScope.launch {
|
serviceScope.launch {
|
||||||
@@ -565,6 +591,55 @@ class WifiAwareMeshService(private val context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Periodic active maintenance: retries connections to discovered but unconnected peers.
|
||||||
|
*/
|
||||||
|
private fun startPeriodicConnectionMaintenance() {
|
||||||
|
serviceScope.launch {
|
||||||
|
Log.d(TAG, "Starting periodic connection maintenance loop")
|
||||||
|
while (isActive) {
|
||||||
|
try {
|
||||||
|
delay(15_000) // Check every 15 seconds
|
||||||
|
if (!isActive) break
|
||||||
|
|
||||||
|
val now = System.currentTimeMillis()
|
||||||
|
// 1. Identify peers that are discovered (recently seen) but not currently connected
|
||||||
|
val recentDiscovered = discoveredTimestamps.filter { (id, ts) ->
|
||||||
|
(now - ts) < 5 * 60 * 1000 // Seen in last 5 minutes
|
||||||
|
}.keys
|
||||||
|
|
||||||
|
// 2. Filter out those who are already connected (either as client or server)
|
||||||
|
val disconnectedPeers = recentDiscovered.filter { peerId ->
|
||||||
|
!peerSockets.containsKey(peerId) && !serverSockets.containsKey(peerId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Attempt reconnection
|
||||||
|
for (peerId in disconnectedPeers) {
|
||||||
|
// Find the PeerHandle for this peerId
|
||||||
|
val handle = handleToPeerId.entries.find { it.value == peerId }?.key ?: continue
|
||||||
|
|
||||||
|
// Rate limit attempts: max 1 per minute per peer
|
||||||
|
val lastAttempt = connectionAttempts[peerId] ?: 0L
|
||||||
|
if (now - lastAttempt < 60_000) continue
|
||||||
|
|
||||||
|
Log.i(TAG, "🔄 Maintenance: attempting reconnect to ${peerId.take(8)}")
|
||||||
|
connectionAttempts[peerId] = now
|
||||||
|
|
||||||
|
// Resend ping to trigger handshake
|
||||||
|
val msgId = (System.nanoTime() and 0x7fffffff).toInt()
|
||||||
|
try {
|
||||||
|
subscribeSession?.sendMessage(handle, msgId, myPeerID.toByteArray())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "Failed to send maintenance ping to ${peerId.take(8)}: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Error in connection maintenance: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles subscriber ping: spawns a server socket and responds with connection info.
|
* Handles subscriber ping: spawns a server socket and responds with connection info.
|
||||||
*
|
*
|
||||||
@@ -819,7 +894,6 @@ class WifiAwareMeshService(private val context: Context) {
|
|||||||
private fun listenToPeer(socket: Socket, initialLogicalPeerId: String) {
|
private fun listenToPeer(socket: Socket, initialLogicalPeerId: String) {
|
||||||
val inStream = socket.getInputStream()
|
val inStream = socket.getInputStream()
|
||||||
val buf = ByteArray(64 * 1024)
|
val buf = ByteArray(64 * 1024)
|
||||||
var routedPeerId: String? = null
|
|
||||||
|
|
||||||
while (isActive) {
|
while (isActive) {
|
||||||
val len = try { inStream.read(buf) } catch (_: IOException) { break }
|
val len = try { inStream.read(buf) } catch (_: IOException) { break }
|
||||||
@@ -829,46 +903,36 @@ class WifiAwareMeshService(private val context: Context) {
|
|||||||
val pkt = BitchatPacket.fromBinaryData(raw) ?: continue
|
val pkt = BitchatPacket.fromBinaryData(raw) ?: continue
|
||||||
|
|
||||||
val senderPeerHex = pkt.senderID?.toHexString()?.take(16) ?: continue
|
val senderPeerHex = pkt.senderID?.toHexString()?.take(16) ?: continue
|
||||||
if (senderPeerHex == myPeerID) continue
|
|
||||||
|
// Deduplicate based on timestamp + sender (standard flood fill protection)
|
||||||
val ts = pkt.timestamp
|
val ts = pkt.timestamp
|
||||||
if (lastTimestamps.put(senderPeerHex, ts) == ts) {
|
if (lastTimestamps.put(senderPeerHex, ts) == ts) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if (routedPeerId == null) {
|
// Route the packet:
|
||||||
routedPeerId = senderPeerHex
|
// - peerID = Originator (who signed it)
|
||||||
peerSockets[routedPeerId] = socket
|
// - relayAddress = Neighbor (who sent it to us over this socket)
|
||||||
}
|
// Note: We do NOT update peerSockets mapping based on senderPeerHex.
|
||||||
|
// The socket belongs to initialLogicalPeerId effectively serving as the "MAC address" layer.
|
||||||
Log.d(TAG, "RX: packet type=${pkt.type} from ${senderPeerHex.take(8)} (bytes=${raw.size})")
|
Log.d(TAG, "RX: packet type=${pkt.type} from ${senderPeerHex.take(8)} via ${initialLogicalPeerId.take(8)} (bytes=${raw.size})")
|
||||||
packetProcessor.processPacket(RoutedPacket(pkt, routedPeerId))
|
packetProcessor.processPacket(RoutedPacket(pkt, senderPeerHex, initialLogicalPeerId))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Breaking out of the loop means the socket is dead or service is stopping.
|
// Breaking out of the loop means the socket is dead or service is stopping.
|
||||||
// We MUST notify the mesh layer so it removes the logical peer immediately to allow reconnection.
|
|
||||||
Log.i(TAG, "Socket loop terminated for ${initialLogicalPeerId.take(8)} removing peer.")
|
Log.i(TAG, "Socket loop terminated for ${initialLogicalPeerId.take(8)} removing peer.")
|
||||||
handlePeerDisconnection(initialLogicalPeerId, routedPeerId)
|
handlePeerDisconnection(initialLogicalPeerId)
|
||||||
socket.closeQuietly()
|
socket.closeQuietly()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handlePeerDisconnection(initialId: String, routedId: String?) {
|
private fun handlePeerDisconnection(initialId: String) {
|
||||||
serviceScope.launch {
|
serviceScope.launch {
|
||||||
Log.d(TAG, "Cleaning up peer: $initialId / $routedId")
|
Log.d(TAG, "Cleaning up peer: $initialId")
|
||||||
|
|
||||||
peerSockets.remove(initialId)?.closeQuietly()
|
peerSockets.remove(initialId)?.closeQuietly()
|
||||||
serverSockets.remove(initialId)?.closeQuietly()
|
serverSockets.remove(initialId)?.closeQuietly()
|
||||||
networkCallbacks.remove(initialId)?.let { runCatching { cm.unregisterNetworkCallback(it) } }
|
networkCallbacks.remove(initialId)?.let { runCatching { cm.unregisterNetworkCallback(it) } }
|
||||||
peerManager.removePeer(initialId)
|
peerManager.removePeer(initialId)
|
||||||
|
|
||||||
routedId?.let { id ->
|
|
||||||
if (id != initialId) {
|
|
||||||
peerSockets.remove(id)?.closeQuietly()
|
|
||||||
serverSockets.remove(id)?.closeQuietly()
|
|
||||||
networkCallbacks.remove(id)?.let { runCatching { cm.unregisterNetworkCallback(it) } }
|
|
||||||
peerManager.removePeer(id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -893,7 +957,7 @@ class WifiAwareMeshService(private val context: Context) {
|
|||||||
ttl = MAX_TTL
|
ttl = MAX_TTL
|
||||||
)
|
)
|
||||||
val signed = signPacketBeforeBroadcast(packet)
|
val signed = signPacketBeforeBroadcast(packet)
|
||||||
broadcastPacket(RoutedPacket(signed))
|
dispatchGlobal(RoutedPacket(signed))
|
||||||
try { gossipSyncManager.onPublicPacketSeen(signed) } catch (_: Exception) { }
|
try { gossipSyncManager.onPublicPacketSeen(signed) } catch (_: Exception) { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -929,7 +993,7 @@ class WifiAwareMeshService(private val context: Context) {
|
|||||||
signature = null,
|
signature = null,
|
||||||
ttl = MAX_TTL
|
ttl = MAX_TTL
|
||||||
)
|
)
|
||||||
broadcastPacket(RoutedPacket(signPacketBeforeBroadcast(pkt)))
|
dispatchGlobal(RoutedPacket(signPacketBeforeBroadcast(pkt)))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to encrypt private message: ${e.message}")
|
Log.e(TAG, "Failed to encrypt private message: ${e.message}")
|
||||||
}
|
}
|
||||||
@@ -965,7 +1029,7 @@ class WifiAwareMeshService(private val context: Context) {
|
|||||||
signature = null,
|
signature = null,
|
||||||
ttl = MAX_TTL
|
ttl = MAX_TTL
|
||||||
)
|
)
|
||||||
broadcastPacket(RoutedPacket(signPacketBeforeBroadcast(pkt)))
|
dispatchGlobal(RoutedPacket(signPacketBeforeBroadcast(pkt)))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to send read receipt: ${e.message}")
|
Log.e(TAG, "Failed to send read receipt: ${e.message}")
|
||||||
}
|
}
|
||||||
@@ -994,7 +1058,7 @@ class WifiAwareMeshService(private val context: Context) {
|
|||||||
)
|
)
|
||||||
val signed = signPacketBeforeBroadcast(pkt)
|
val signed = signPacketBeforeBroadcast(pkt)
|
||||||
val transferId = sha256Hex(payload)
|
val transferId = sha256Hex(payload)
|
||||||
broadcastPacket(RoutedPacket(signed, transferId = transferId))
|
dispatchGlobal(RoutedPacket(signed, transferId = transferId))
|
||||||
try { gossipSyncManager.onPublicPacketSeen(signed) } catch (_: Exception) { }
|
try { gossipSyncManager.onPublicPacketSeen(signed) } catch (_: Exception) { }
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -1031,7 +1095,7 @@ class WifiAwareMeshService(private val context: Context) {
|
|||||||
)
|
)
|
||||||
val signed = signPacketBeforeBroadcast(pkt)
|
val signed = signPacketBeforeBroadcast(pkt)
|
||||||
val transferId = sha256Hex(tlv)
|
val transferId = sha256Hex(tlv)
|
||||||
broadcastPacket(RoutedPacket(signed, transferId = transferId))
|
dispatchGlobal(RoutedPacket(signed, transferId = transferId))
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "sendFilePrivate failed: ${e.message}", e)
|
Log.e(TAG, "sendFilePrivate failed: ${e.message}", e)
|
||||||
@@ -1080,7 +1144,7 @@ class WifiAwareMeshService(private val context: Context) {
|
|||||||
)
|
)
|
||||||
val signed = signPacketBeforeBroadcast(announcePacket)
|
val signed = signPacketBeforeBroadcast(announcePacket)
|
||||||
|
|
||||||
broadcastPacket(RoutedPacket(signed))
|
dispatchGlobal(RoutedPacket(signed))
|
||||||
try { gossipSyncManager.onPublicPacketSeen(signed) } catch (_: Exception) { }
|
try { gossipSyncManager.onPublicPacketSeen(signed) } catch (_: Exception) { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1105,7 +1169,7 @@ class WifiAwareMeshService(private val context: Context) {
|
|||||||
)
|
)
|
||||||
val signed = signPacketBeforeBroadcast(packet)
|
val signed = signPacketBeforeBroadcast(packet)
|
||||||
|
|
||||||
broadcastPacket(RoutedPacket(signed))
|
dispatchGlobal(RoutedPacket(signed))
|
||||||
peerManager.markPeerAsAnnouncedTo(peerID)
|
peerManager.markPeerAsAnnouncedTo(peerID)
|
||||||
try { gossipSyncManager.onPublicPacketSeen(signed) } catch (_: Exception) { }
|
try { gossipSyncManager.onPublicPacketSeen(signed) } catch (_: Exception) { }
|
||||||
}
|
}
|
||||||
@@ -1121,7 +1185,7 @@ class WifiAwareMeshService(private val context: Context) {
|
|||||||
senderID = myPeerID,
|
senderID = myPeerID,
|
||||||
payload = nickname.toByteArray()
|
payload = nickname.toByteArray()
|
||||||
)
|
)
|
||||||
broadcastPacket(RoutedPacket(signPacketBeforeBroadcast(packet)))
|
dispatchGlobal(RoutedPacket(signPacketBeforeBroadcast(packet)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return Mapping of peer IDs to nicknames. */
|
/** @return Mapping of peer IDs to nicknames. */
|
||||||
|
|||||||
Reference in New Issue
Block a user