mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-24 22:25:19 +00:00
wifi aware working
This commit is contained in:
@@ -793,7 +793,8 @@ class MainActivity : OrientationAwareActivity() {
|
||||
|
||||
// Check if Bluetooth was disabled while app was backgrounded
|
||||
val currentBluetoothStatus = bluetoothStatusManager.checkBluetoothStatus()
|
||||
if (currentBluetoothStatus != BluetoothStatus.ENABLED && !mainViewModel.isBluetoothCheckSkipped.value) {
|
||||
val bleRequired = try { com.bitchat.android.ui.debug.DebugPreferenceManager.getBleEnabled(true) } catch (_: Exception) { true }
|
||||
if (bleRequired && currentBluetoothStatus != BluetoothStatus.ENABLED && !mainViewModel.isBluetoothCheckSkipped.value) {
|
||||
Log.w("MainActivity", "Bluetooth disabled while app was backgrounded")
|
||||
mainViewModel.updateBluetoothStatus(currentBluetoothStatus)
|
||||
mainViewModel.updateOnboardingState(OnboardingState.BLUETOOTH_CHECK)
|
||||
|
||||
@@ -88,22 +88,49 @@ class BluetoothConnectionManager(
|
||||
// Public property for address-peer mapping
|
||||
val addressPeerMap get() = connectionTracker.addressPeerMap
|
||||
|
||||
private fun isBleTransportEnabled(): Boolean {
|
||||
return try {
|
||||
com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().bleEnabled.value
|
||||
} catch (_: Exception) {
|
||||
try { com.bitchat.android.ui.debug.DebugPreferenceManager.getBleEnabled(true) } catch (_: Exception) { true }
|
||||
}
|
||||
}
|
||||
|
||||
private fun isGattServerEnabled(): Boolean {
|
||||
return isBleTransportEnabled() &&
|
||||
(try { com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().gattServerEnabled.value } catch (_: Exception) { true })
|
||||
}
|
||||
|
||||
private fun isGattClientEnabled(): Boolean {
|
||||
return isBleTransportEnabled() &&
|
||||
(try { com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().gattClientEnabled.value } catch (_: Exception) { true })
|
||||
}
|
||||
|
||||
init {
|
||||
powerManager.delegate = this
|
||||
// Observe debug settings to enforce role state while active
|
||||
try {
|
||||
val dbg = com.bitchat.android.ui.debug.DebugSettingsManager.getInstance()
|
||||
// Master transport enable/disable
|
||||
connectionScope.launch {
|
||||
dbg.bleEnabled.collect { enabled ->
|
||||
if (enabled) return@collect
|
||||
if (isActive) {
|
||||
disableTransport()
|
||||
}
|
||||
}
|
||||
}
|
||||
// Role enable/disable
|
||||
connectionScope.launch {
|
||||
dbg.gattServerEnabled.collect { enabled ->
|
||||
if (!isActive) return@collect
|
||||
if (enabled) startServer() else stopServer()
|
||||
if (enabled && isBleTransportEnabled()) startServer() else stopServer()
|
||||
}
|
||||
}
|
||||
connectionScope.launch {
|
||||
dbg.gattClientEnabled.collect { enabled ->
|
||||
if (!isActive) return@collect
|
||||
if (enabled) startClient() else stopClient()
|
||||
if (enabled && isBleTransportEnabled()) startClient() else stopClient()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,6 +190,12 @@ class BluetoothConnectionManager(
|
||||
*/
|
||||
fun startServices(): Boolean {
|
||||
Log.i(TAG, "Starting power-optimized Bluetooth services...")
|
||||
|
||||
if (!isBleTransportEnabled()) {
|
||||
Log.i(TAG, "BLE transport disabled by debug settings; not starting Bluetooth services")
|
||||
disableTransport()
|
||||
return false
|
||||
}
|
||||
|
||||
if (!permissionManager.hasBluetoothPermissions()) {
|
||||
Log.e(TAG, "Missing Bluetooth permissions")
|
||||
@@ -197,9 +230,8 @@ class BluetoothConnectionManager(
|
||||
powerManager.start()
|
||||
|
||||
// Start server/client based on debug settings
|
||||
val dbg = try { com.bitchat.android.ui.debug.DebugSettingsManager.getInstance() } catch (_: Exception) { null }
|
||||
val startServer = dbg?.gattServerEnabled?.value != false
|
||||
val startClient = dbg?.gattClientEnabled?.value != false
|
||||
val startServer = isGattServerEnabled()
|
||||
val startClient = isGattClientEnabled()
|
||||
|
||||
if (startServer) {
|
||||
if (!serverManager.start()) {
|
||||
@@ -234,6 +266,19 @@ class BluetoothConnectionManager(
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable BLE without cancelling this manager's coroutine scope, so it can be re-enabled.
|
||||
*/
|
||||
fun disableTransport() {
|
||||
Log.i(TAG, "Disabling BLE transport")
|
||||
isActive = false
|
||||
connectionScope.launch {
|
||||
clientManager.stop()
|
||||
serverManager.stop()
|
||||
connectionTracker.stop()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop all Bluetooth services with proper cleanup
|
||||
@@ -279,7 +324,7 @@ class BluetoothConnectionManager(
|
||||
* Automatically fragments large packets to fit within BLE MTU limits
|
||||
*/
|
||||
fun broadcastPacket(routed: RoutedPacket) {
|
||||
if (!isActive) return
|
||||
if (!isActive || !isBleTransportEnabled()) return
|
||||
|
||||
packetBroadcaster.broadcastPacket(
|
||||
routed,
|
||||
@@ -289,7 +334,7 @@ class BluetoothConnectionManager(
|
||||
}
|
||||
|
||||
fun sendToPeer(peerID: String, routed: RoutedPacket): Boolean {
|
||||
if (!isActive) return false
|
||||
if (!isActive || !isBleTransportEnabled()) return false
|
||||
return packetBroadcaster.sendToPeer(
|
||||
peerID,
|
||||
routed,
|
||||
@@ -306,7 +351,7 @@ class BluetoothConnectionManager(
|
||||
* Send a packet directly to a specific peer, without broadcasting to others.
|
||||
*/
|
||||
fun sendPacketToPeer(peerID: String, packet: BitchatPacket): Boolean {
|
||||
if (!isActive) return false
|
||||
if (!isActive || !isBleTransportEnabled()) return false
|
||||
return packetBroadcaster.sendPacketToPeer(
|
||||
RoutedPacket(packet),
|
||||
peerID,
|
||||
@@ -317,9 +362,15 @@ class BluetoothConnectionManager(
|
||||
|
||||
|
||||
// Expose role controls for debug UI
|
||||
fun startServer() { connectionScope.launch { serverManager.start() } }
|
||||
fun startServer() {
|
||||
if (!isActive || !isBleTransportEnabled()) return
|
||||
connectionScope.launch { if (isGattServerEnabled()) serverManager.start() }
|
||||
}
|
||||
fun stopServer() { connectionScope.launch { serverManager.stop() } }
|
||||
fun startClient() { connectionScope.launch { clientManager.start() } }
|
||||
fun startClient() {
|
||||
if (!isActive || !isBleTransportEnabled()) return
|
||||
connectionScope.launch { if (isGattClientEnabled()) clientManager.start() }
|
||||
}
|
||||
fun stopClient() { connectionScope.launch { clientManager.stop() } }
|
||||
|
||||
// Inject nickname resolver for broadcaster logs
|
||||
@@ -347,7 +398,10 @@ class BluetoothConnectionManager(
|
||||
/**
|
||||
* Public: connect/disconnect helpers for debug UI
|
||||
*/
|
||||
fun connectToAddress(address: String): Boolean = clientManager.connectToAddress(address)
|
||||
fun connectToAddress(address: String): Boolean {
|
||||
if (!isActive || !isBleTransportEnabled()) return false
|
||||
return clientManager.connectToAddress(address)
|
||||
}
|
||||
fun disconnectAddress(address: String) { connectionTracker.disconnectDevice(address) }
|
||||
|
||||
|
||||
@@ -358,10 +412,10 @@ class BluetoothConnectionManager(
|
||||
clientManager.stop()
|
||||
serverManager.stop()
|
||||
delay(200)
|
||||
if (isActive) {
|
||||
if (isActive && isBleTransportEnabled()) {
|
||||
// Restart managers if service is active
|
||||
serverManager.start()
|
||||
clientManager.start()
|
||||
if (isGattServerEnabled()) serverManager.start()
|
||||
if (isGattClientEnabled()) clientManager.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -396,11 +450,17 @@ class BluetoothConnectionManager(
|
||||
Log.i(TAG, "Power mode changed to: $newMode")
|
||||
|
||||
connectionScope.launch {
|
||||
if (!isActive || !isBleTransportEnabled()) {
|
||||
serverManager.stop()
|
||||
clientManager.stop()
|
||||
return@launch
|
||||
}
|
||||
|
||||
// Avoid rapid scan restarts by checking if we need to change scan behavior
|
||||
val wasUsingDutyCycle = powerManager.shouldUseDutyCycle()
|
||||
|
||||
// Update advertising with new power settings if server enabled
|
||||
val serverEnabled = try { com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().gattServerEnabled.value } catch (_: Exception) { true }
|
||||
val serverEnabled = isGattServerEnabled()
|
||||
if (serverEnabled) {
|
||||
serverManager.restartAdvertising()
|
||||
} else {
|
||||
@@ -411,7 +471,7 @@ class BluetoothConnectionManager(
|
||||
val nowUsingDutyCycle = powerManager.shouldUseDutyCycle()
|
||||
if (wasUsingDutyCycle != nowUsingDutyCycle) {
|
||||
Log.d(TAG, "Duty cycle behavior changed (${wasUsingDutyCycle} -> ${nowUsingDutyCycle}), restarting scan")
|
||||
val clientEnabled = try { com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().gattClientEnabled.value } catch (_: Exception) { true }
|
||||
val clientEnabled = isGattClientEnabled()
|
||||
if (clientEnabled) {
|
||||
clientManager.restartScanning()
|
||||
} else {
|
||||
@@ -427,6 +487,10 @@ class BluetoothConnectionManager(
|
||||
}
|
||||
|
||||
override fun onScanStateChanged(shouldScan: Boolean) {
|
||||
if (!isActive || !isBleTransportEnabled()) {
|
||||
clientManager.onScanStateChanged(false)
|
||||
return
|
||||
}
|
||||
clientManager.onScanStateChanged(shouldScan)
|
||||
}
|
||||
|
||||
|
||||
@@ -39,11 +39,28 @@ class BluetoothGattClientManager(
|
||||
context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
|
||||
private val bluetoothAdapter: BluetoothAdapter? = bluetoothManager.adapter
|
||||
private val bleScanner: BluetoothLeScanner? = bluetoothAdapter?.bluetoothLeScanner
|
||||
|
||||
private fun isBleTransportEnabled(): Boolean {
|
||||
return try {
|
||||
com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().bleEnabled.value
|
||||
} catch (_: Exception) {
|
||||
try { com.bitchat.android.ui.debug.DebugPreferenceManager.getBleEnabled(true) } catch (_: Exception) { true }
|
||||
}
|
||||
}
|
||||
|
||||
private fun isClientRoleEnabled(): Boolean {
|
||||
return isBleTransportEnabled() &&
|
||||
(try { com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().gattClientEnabled.value } catch (_: Exception) { true })
|
||||
}
|
||||
|
||||
/**
|
||||
* Public: Connect to a device by MAC address (for debug UI)
|
||||
*/
|
||||
fun connectToAddress(deviceAddress: String): Boolean {
|
||||
if (!isClientRoleEnabled()) {
|
||||
Log.i(TAG, "connectToAddress skipped: BLE client disabled")
|
||||
return false
|
||||
}
|
||||
val device = bluetoothAdapter?.getRemoteDevice(deviceAddress)
|
||||
return if (device != null) {
|
||||
val rssi = connectionTracker.getBestRSSI(deviceAddress) ?: -50
|
||||
@@ -75,12 +92,10 @@ class BluetoothGattClientManager(
|
||||
*/
|
||||
fun start(): Boolean {
|
||||
// Respect debug setting
|
||||
try {
|
||||
if (!com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().gattClientEnabled.value) {
|
||||
Log.i(TAG, "Client start skipped: GATT Client disabled in debug settings")
|
||||
return false
|
||||
}
|
||||
} catch (_: Exception) { }
|
||||
if (!isClientRoleEnabled()) {
|
||||
Log.i(TAG, "Client start skipped: BLE/GATT Client disabled in debug settings")
|
||||
return false
|
||||
}
|
||||
|
||||
if (isActive) {
|
||||
Log.d(TAG, "GATT client already active; start is a no-op")
|
||||
@@ -150,7 +165,7 @@ class BluetoothGattClientManager(
|
||||
* Handle scan state changes from power manager
|
||||
*/
|
||||
fun onScanStateChanged(shouldScan: Boolean) {
|
||||
val enabled = try { com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().gattClientEnabled.value } catch (_: Exception) { true }
|
||||
val enabled = isClientRoleEnabled()
|
||||
if (shouldScan && enabled) {
|
||||
startScanning()
|
||||
} else {
|
||||
@@ -199,7 +214,7 @@ class BluetoothGattClientManager(
|
||||
@Suppress("DEPRECATION")
|
||||
private fun startScanning() {
|
||||
// Respect debug setting
|
||||
val enabled = try { com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().gattClientEnabled.value } catch (_: Exception) { true }
|
||||
val enabled = isClientRoleEnabled()
|
||||
if (!permissionManager.hasBluetoothPermissions() || bleScanner == null || !isActive || !enabled) return
|
||||
|
||||
// Rate limit scan starts to prevent "scanning too frequently" errors
|
||||
@@ -217,7 +232,7 @@ class BluetoothGattClientManager(
|
||||
// Schedule delayed scan start
|
||||
connectionScope.launch {
|
||||
delay(remainingWait)
|
||||
if (isActive && !isCurrentlyScanning) {
|
||||
if (isActive && !isCurrentlyScanning && isClientRoleEnabled()) {
|
||||
startScanning()
|
||||
}
|
||||
}
|
||||
@@ -402,6 +417,7 @@ class BluetoothGattClientManager(
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
private fun connectToDevice(device: BluetoothDevice, rssi: Int, peerID: String? = null) {
|
||||
if (!isClientRoleEnabled()) return
|
||||
if (!permissionManager.hasBluetoothPermissions()) return
|
||||
|
||||
val deviceAddress = device.address
|
||||
@@ -562,7 +578,7 @@ class BluetoothGattClientManager(
|
||||
*/
|
||||
fun restartScanning() {
|
||||
// Respect debug setting
|
||||
val enabled = try { com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().gattClientEnabled.value } catch (_: Exception) { true }
|
||||
val enabled = isClientRoleEnabled()
|
||||
if (!isActive || !enabled) return
|
||||
|
||||
connectionScope.launch {
|
||||
|
||||
@@ -46,6 +46,19 @@ class BluetoothGattServerManager(
|
||||
// State management
|
||||
private var isActive = false
|
||||
|
||||
private fun isBleTransportEnabled(): Boolean {
|
||||
return try {
|
||||
com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().bleEnabled.value
|
||||
} catch (_: Exception) {
|
||||
try { com.bitchat.android.ui.debug.DebugPreferenceManager.getBleEnabled(true) } catch (_: Exception) { true }
|
||||
}
|
||||
}
|
||||
|
||||
private fun isServerRoleEnabled(): Boolean {
|
||||
return isBleTransportEnabled() &&
|
||||
(try { com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().gattServerEnabled.value } catch (_: Exception) { true })
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect a specific device (used by ConnectionManager to enforce overall limits)
|
||||
*/
|
||||
@@ -62,12 +75,10 @@ class BluetoothGattServerManager(
|
||||
*/
|
||||
fun start(): Boolean {
|
||||
// Respect debug setting
|
||||
try {
|
||||
if (!com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().gattServerEnabled.value) {
|
||||
Log.i(TAG, "Server start skipped: GATT Server disabled in debug settings")
|
||||
return false
|
||||
}
|
||||
} catch (_: Exception) { }
|
||||
if (!isServerRoleEnabled()) {
|
||||
Log.i(TAG, "Server start skipped: BLE/GATT Server disabled in debug settings")
|
||||
return false
|
||||
}
|
||||
|
||||
if (isActive) {
|
||||
Log.d(TAG, "GATT server already active; start is a no-op")
|
||||
@@ -322,7 +333,7 @@ class BluetoothGattServerManager(
|
||||
@Suppress("DEPRECATION")
|
||||
private fun startAdvertising() {
|
||||
// Respect debug setting
|
||||
val enabled = try { com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().gattServerEnabled.value } catch (_: Exception) { true }
|
||||
val enabled = isServerRoleEnabled()
|
||||
|
||||
// Guard conditions – never throw here to avoid crashing the app from a background coroutine
|
||||
if (!permissionManager.hasBluetoothPermissions()) {
|
||||
@@ -412,7 +423,7 @@ class BluetoothGattServerManager(
|
||||
*/
|
||||
fun restartAdvertising() {
|
||||
// Respect debug setting
|
||||
val enabled = try { com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().gattServerEnabled.value } catch (_: Exception) { true }
|
||||
val enabled = isServerRoleEnabled()
|
||||
if (!isActive || !enabled) {
|
||||
stopAdvertising()
|
||||
return
|
||||
|
||||
@@ -71,6 +71,7 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic
|
||||
|
||||
// Coroutines
|
||||
private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
private var announceJob: Job? = null
|
||||
// Tracks whether this instance has been terminated via stopServices()
|
||||
private var terminated = false
|
||||
|
||||
@@ -114,7 +115,9 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic
|
||||
}
|
||||
|
||||
com.bitchat.android.service.MeshServiceHolder.setGossipManager(gossipSyncManager)
|
||||
TransportBridgeService.register("BLE", this)
|
||||
if (isBleTransportEnabled()) {
|
||||
TransportBridgeService.register("BLE", this)
|
||||
}
|
||||
|
||||
// Inject dynamic direct connection check into PeerManager
|
||||
// Matches iOS logic: checks if we have an active hardware mapping for this peer
|
||||
@@ -126,23 +129,35 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic
|
||||
}
|
||||
|
||||
override fun send(packet: RoutedPacket) {
|
||||
if (!isBleTransportEnabled()) return
|
||||
connectionManager.broadcastPacket(packet)
|
||||
}
|
||||
|
||||
override fun sendToPeer(peerID: String, packet: BitchatPacket) {
|
||||
if (!isBleTransportEnabled()) return
|
||||
connectionManager.sendPacketToPeer(peerID, packet)
|
||||
}
|
||||
|
||||
private fun broadcastRoutedPacket(routed: RoutedPacket) {
|
||||
if (!isBleTransportEnabled()) return
|
||||
connectionManager.broadcastPacket(routed)
|
||||
TransportBridgeService.broadcast("BLE", routed)
|
||||
}
|
||||
|
||||
private fun sendPacketToPeerAcrossTransports(peerID: String, packet: BitchatPacket): Boolean {
|
||||
if (!isBleTransportEnabled()) return false
|
||||
val sentOverBle = connectionManager.sendPacketToPeer(peerID, packet)
|
||||
TransportBridgeService.sendToPeer("BLE", peerID, packet)
|
||||
return sentOverBle
|
||||
}
|
||||
|
||||
private fun isBleTransportEnabled(): Boolean {
|
||||
return try {
|
||||
com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().bleEnabled.value
|
||||
} catch (_: Exception) {
|
||||
try { com.bitchat.android.ui.debug.DebugPreferenceManager.getBleEnabled(true) } catch (_: Exception) { true }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start periodic debug logging every 10 seconds
|
||||
@@ -169,7 +184,8 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic
|
||||
* Send broadcast announcement every 30 seconds
|
||||
*/
|
||||
private fun sendPeriodicBroadcastAnnounce() {
|
||||
serviceScope.launch {
|
||||
announceJob?.cancel()
|
||||
announceJob = serviceScope.launch {
|
||||
Log.d(TAG, "Starting periodic announce loop")
|
||||
while (isActive) {
|
||||
try {
|
||||
@@ -198,7 +214,7 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic
|
||||
peerManager.delegate = object : PeerManagerDelegate {
|
||||
override fun onPeerListUpdated(peerIDs: List<String>) {
|
||||
// Update process-wide state first
|
||||
try { com.bitchat.android.services.AppStateStore.setPeers(peerIDs) } catch (_: Exception) { }
|
||||
try { com.bitchat.android.services.AppStateStore.setTransportPeers("BLE", peerIDs) } catch (_: Exception) { }
|
||||
// Then notify UI delegate if attached
|
||||
delegate?.didUpdatePeerList(peerIDs)
|
||||
}
|
||||
@@ -650,6 +666,13 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic
|
||||
Log.w(TAG, "Mesh service already active, ignoring duplicate start request")
|
||||
return
|
||||
}
|
||||
if (!isBleTransportEnabled()) {
|
||||
Log.i(TAG, "BLE transport disabled by debug settings; not starting mesh service")
|
||||
connectionManager.disableTransport()
|
||||
TransportBridgeService.unregister("BLE")
|
||||
try { com.bitchat.android.services.AppStateStore.clearTransportPeers("BLE") } catch (_: Exception) { }
|
||||
return
|
||||
}
|
||||
if (terminated) {
|
||||
// This instance's scope was cancelled previously; refuse to start to avoid using dead scopes.
|
||||
Log.e(TAG, "Mesh service instance was terminated; create a new instance instead of restarting")
|
||||
@@ -660,6 +683,7 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic
|
||||
|
||||
if (connectionManager.startServices()) {
|
||||
isActive = true
|
||||
TransportBridgeService.register("BLE", this)
|
||||
|
||||
// Start periodic announcements for peer discovery and connectivity
|
||||
sendPeriodicBroadcastAnnounce()
|
||||
@@ -671,6 +695,29 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic
|
||||
Log.e(TAG, "Failed to start Bluetooth services")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the debug master transport toggle without destroying this mesh instance.
|
||||
*/
|
||||
fun setBleTransportEnabled(enabled: Boolean) {
|
||||
if (enabled) {
|
||||
startServices()
|
||||
} else {
|
||||
pauseServicesForTransportDisable()
|
||||
}
|
||||
}
|
||||
|
||||
private fun pauseServicesForTransportDisable() {
|
||||
Log.i(TAG, "Disabling BLE mesh transport")
|
||||
isActive = false
|
||||
announceJob?.cancel()
|
||||
announceJob = null
|
||||
try { gossipSyncManager.stop() } catch (_: Exception) { }
|
||||
TransportBridgeService.unregister("BLE")
|
||||
try { com.bitchat.android.services.AppStateStore.clearTransportPeers("BLE") } catch (_: Exception) { }
|
||||
connectionManager.disableTransport()
|
||||
try { peerManager.refreshPeerList() } catch (_: Exception) { }
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop all mesh services
|
||||
@@ -683,7 +730,10 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic
|
||||
|
||||
Log.i(TAG, "Stopping Bluetooth mesh service")
|
||||
isActive = false
|
||||
announceJob?.cancel()
|
||||
announceJob = null
|
||||
TransportBridgeService.unregister("BLE")
|
||||
try { com.bitchat.android.services.AppStateStore.clearTransportPeers("BLE") } catch (_: Exception) { }
|
||||
|
||||
// Send leave announcement
|
||||
sendLeaveAnnouncement()
|
||||
|
||||
@@ -16,6 +16,7 @@ import com.bitchat.android.protocol.MessageType
|
||||
import com.bitchat.android.protocol.SpecialRecipients
|
||||
import com.bitchat.android.service.TransportBridgeService
|
||||
import com.bitchat.android.sync.GossipSyncManager
|
||||
import com.bitchat.android.util.toHexString
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
@@ -144,12 +145,14 @@ class MeshCore(
|
||||
private fun setupDelegates() {
|
||||
peerManager.delegate = object : PeerManagerDelegate {
|
||||
override fun onPeerListUpdated(peerIDs: List<String>) {
|
||||
try { com.bitchat.android.services.AppStateStore.setTransportPeers(transport.id, peerIDs) } catch (_: Exception) { }
|
||||
delegate?.didUpdatePeerList(peerIDs)
|
||||
}
|
||||
|
||||
override fun onPeerRemoved(peerID: String) {
|
||||
try { gossipSyncManager.removeAnnouncementForPeer(peerID) } catch (_: Exception) { }
|
||||
try { encryptionService.removePeer(peerID) } catch (_: Exception) { }
|
||||
try { peerManager.refreshPeerList() } catch (_: Exception) { }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -395,9 +398,9 @@ class MeshCore(
|
||||
}
|
||||
|
||||
override fun sendToPeer(peerID: String, routed: RoutedPacket): Boolean {
|
||||
transport.sendPacketToPeer(peerID, routed.packet)
|
||||
val sent = transport.sendPacketToPeer(peerID, routed.packet)
|
||||
TransportBridgeService.sendToPeer(transport.id, peerID, routed.packet)
|
||||
return true
|
||||
return sent
|
||||
}
|
||||
|
||||
override fun handleRequestSync(routed: RoutedPacket) {
|
||||
@@ -560,7 +563,7 @@ class MeshCore(
|
||||
return@launch
|
||||
}
|
||||
val announcement = IdentityAnnouncement(nickname, staticKey, signingKey)
|
||||
val tlvPayload = announcement.encode() ?: return@launch
|
||||
val tlvPayload = buildAnnouncementPayload(announcement, nickname) ?: return@launch
|
||||
val announcePacket = BitchatPacket(
|
||||
type = MessageType.ANNOUNCE.value,
|
||||
ttl = maxTtl,
|
||||
@@ -581,7 +584,7 @@ class MeshCore(
|
||||
val staticKey = encryptionService.getStaticPublicKey() ?: return
|
||||
val signingKey = encryptionService.getSigningPublicKey() ?: return
|
||||
val announcement = IdentityAnnouncement(nickname, staticKey, signingKey)
|
||||
val tlvPayload = announcement.encode() ?: return
|
||||
val tlvPayload = buildAnnouncementPayload(announcement, nickname) ?: return
|
||||
val packet = BitchatPacket(
|
||||
type = MessageType.ANNOUNCE.value,
|
||||
ttl = maxTtl,
|
||||
@@ -594,6 +597,30 @@ class MeshCore(
|
||||
try { gossipSyncManager.onPublicPacketSeen(signedPacket) } catch (_: Exception) { }
|
||||
}
|
||||
|
||||
private fun buildAnnouncementPayload(announcement: IdentityAnnouncement, nickname: String): ByteArray? {
|
||||
var tlvPayload = announcement.encode() ?: return null
|
||||
val directPeersForGossip = getDirectPeerIDsForGossip()
|
||||
try {
|
||||
if (directPeersForGossip.isNotEmpty()) {
|
||||
tlvPayload += com.bitchat.android.services.meshgraph.GossipTLV.encodeNeighbors(directPeersForGossip)
|
||||
}
|
||||
com.bitchat.android.services.meshgraph.MeshGraphService.getInstance()
|
||||
.updateFromAnnouncement(myPeerID, nickname, directPeersForGossip, System.currentTimeMillis().toULong())
|
||||
} catch (_: Exception) { }
|
||||
return tlvPayload
|
||||
}
|
||||
|
||||
private fun getDirectPeerIDsForGossip(): List<String> {
|
||||
return try {
|
||||
val verifiedDirect = peerManager.getVerifiedPeers()
|
||||
.filter { it.value.isDirectConnection }
|
||||
.keys
|
||||
(verifiedDirect + directPeers).distinct().take(10)
|
||||
} catch (_: Exception) {
|
||||
directPeers.toList().take(10)
|
||||
}
|
||||
}
|
||||
|
||||
fun sendLeaveAnnouncement() {
|
||||
val payload = hooks.leavePayloadProvider?.invoke() ?: byteArrayOf()
|
||||
val packet = BitchatPacket(
|
||||
@@ -734,12 +761,33 @@ class MeshCore(
|
||||
|
||||
private fun signPacketBeforeBroadcast(packet: BitchatPacket): BitchatPacket {
|
||||
return try {
|
||||
val packetDataForSigning = packet.toBinaryDataForSigning() ?: return packet
|
||||
val withRoute = try {
|
||||
val recipient = packet.recipientID
|
||||
if (recipient != null && !recipient.contentEquals(SpecialRecipients.BROADCAST)) {
|
||||
val destination = recipient.toHexString()
|
||||
val path = com.bitchat.android.services.meshgraph.RoutePlanner.shortestPath(myPeerID, destination)
|
||||
if (path != null && path.size >= 3) {
|
||||
val intermediates = path.subList(1, path.size - 1)
|
||||
packet.copy(
|
||||
route = intermediates.map { MeshPacketUtils.hexStringToByteArray(it) },
|
||||
version = 2u
|
||||
)
|
||||
} else {
|
||||
packet.copy(route = null)
|
||||
}
|
||||
} else {
|
||||
packet
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
packet
|
||||
}
|
||||
|
||||
val packetDataForSigning = withRoute.toBinaryDataForSigning() ?: return withRoute
|
||||
val signature = encryptionService.signData(packetDataForSigning)
|
||||
if (signature != null) {
|
||||
packet.copy(signature = signature)
|
||||
withRoute.copy(signature = signature)
|
||||
} else {
|
||||
packet
|
||||
withRoute
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
packet
|
||||
|
||||
@@ -11,7 +11,7 @@ interface MeshTransport {
|
||||
|
||||
fun broadcastPacket(routed: RoutedPacket)
|
||||
|
||||
fun sendPacketToPeer(peerID: String, packet: BitchatPacket)
|
||||
fun sendPacketToPeer(peerID: String, packet: BitchatPacket): Boolean
|
||||
|
||||
fun cancelTransfer(transferId: String): Boolean = false
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro
|
||||
|
||||
companion object {
|
||||
private const val TAG = "MessageHandler"
|
||||
private const val ANNOUNCE_CLOCK_SKEW_TOLERANCE_MS = 10 * 60 * 1000L
|
||||
}
|
||||
|
||||
// Delegate for callbacks
|
||||
@@ -220,12 +221,15 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro
|
||||
|
||||
if (peerID == myPeerID) return false
|
||||
|
||||
// Ignore stale announcements older than STALE_PEER_TIMEOUT
|
||||
// Peers use wall-clock packet timestamps; tolerate moderate device clock skew
|
||||
// during identity learning, or later signed messages cannot be verified.
|
||||
val now = System.currentTimeMillis()
|
||||
val age = now - packet.timestamp.toLong()
|
||||
if (age > com.bitchat.android.util.AppConstants.Mesh.STALE_PEER_TIMEOUT_MS) {
|
||||
Log.w(TAG, "Ignoring stale ANNOUNCE from ${peerID.take(8)} (age=${age}ms > ${com.bitchat.android.util.AppConstants.Mesh.STALE_PEER_TIMEOUT_MS}ms)")
|
||||
val clockSkewMs = kotlin.math.abs(now - packet.timestamp.toLong())
|
||||
if (clockSkewMs > ANNOUNCE_CLOCK_SKEW_TOLERANCE_MS) {
|
||||
Log.w(TAG, "Ignoring ANNOUNCE from ${peerID.take(8)} with excessive clock skew (${clockSkewMs}ms > ${ANNOUNCE_CLOCK_SKEW_TOLERANCE_MS}ms)")
|
||||
return false
|
||||
} else if (clockSkewMs > com.bitchat.android.util.AppConstants.Mesh.STALE_PEER_TIMEOUT_MS) {
|
||||
Log.w(TAG, "Accepting ANNOUNCE from ${peerID.take(8)} within clock skew tolerance (${clockSkewMs}ms)")
|
||||
}
|
||||
|
||||
// Try to decode as iOS-compatible IdentityAnnouncement with TLV format
|
||||
|
||||
@@ -23,6 +23,23 @@ class PermissionManager(private val context: Context) {
|
||||
|
||||
private val sharedPrefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
|
||||
private fun shouldRequireWifiAwarePermission(): Boolean {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return false
|
||||
val enabled = try {
|
||||
com.bitchat.android.ui.debug.DebugPreferenceManager.getWifiAwareEnabled(false)
|
||||
} catch (_: Exception) {
|
||||
false
|
||||
}
|
||||
if (!enabled) return false
|
||||
|
||||
val hasFeature = try {
|
||||
context.packageManager.hasSystemFeature(PackageManager.FEATURE_WIFI_AWARE)
|
||||
} catch (_: Exception) {
|
||||
false
|
||||
}
|
||||
return hasFeature
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this is the first time the user is launching the app
|
||||
*/
|
||||
@@ -70,7 +87,7 @@ class PermissionManager(private val context: Context) {
|
||||
))
|
||||
|
||||
// Wi‑Fi Aware: Android 13+ requires NEARBY_WIFI_DEVICES runtime permission
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
if (shouldRequireWifiAwarePermission()) {
|
||||
permissions.add(Manifest.permission.NEARBY_WIFI_DEVICES)
|
||||
}
|
||||
|
||||
@@ -215,7 +232,7 @@ class PermissionManager(private val context: Context) {
|
||||
)
|
||||
|
||||
// Wi‑Fi Aware category (Android 13+)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
if (shouldRequireWifiAwarePermission()) {
|
||||
val wifiAwarePermissions = listOf(Manifest.permission.NEARBY_WIFI_DEVICES)
|
||||
categories.add(
|
||||
PermissionCategory(
|
||||
|
||||
@@ -226,6 +226,15 @@ class MeshForegroundService : Service() {
|
||||
|
||||
private fun ensureMeshStarted() {
|
||||
if (isShuttingDown) return
|
||||
val bleEnabled = try {
|
||||
com.bitchat.android.ui.debug.DebugPreferenceManager.getBleEnabled(true)
|
||||
} catch (_: Exception) {
|
||||
true
|
||||
}
|
||||
if (!bleEnabled) {
|
||||
try { meshService?.setBleTransportEnabled(false) } catch (_: Exception) { }
|
||||
return
|
||||
}
|
||||
if (!hasBluetoothPermissions()) return
|
||||
try {
|
||||
android.util.Log.d("MeshForegroundService", "Ensuring mesh service is started")
|
||||
|
||||
@@ -3,6 +3,10 @@ package com.bitchat.android.service
|
||||
import android.util.Log
|
||||
import com.bitchat.android.model.RoutedPacket
|
||||
import com.bitchat.android.protocol.BitchatPacket
|
||||
import com.bitchat.android.util.toHexString
|
||||
import java.security.MessageDigest
|
||||
import java.util.Collections
|
||||
import java.util.LinkedHashMap
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
/**
|
||||
@@ -14,6 +18,8 @@ import java.util.concurrent.ConcurrentHashMap
|
||||
*/
|
||||
object TransportBridgeService {
|
||||
private const val TAG = "TransportBridgeService"
|
||||
private const val MAX_SEEN_PACKETS = 4096
|
||||
private const val SEEN_PACKET_TTL_MS = 5 * 60 * 1000L
|
||||
|
||||
/**
|
||||
* Interface that any transport layer (BLE, WiFi, Tor, etc.) must implement
|
||||
@@ -32,6 +38,13 @@ object TransportBridgeService {
|
||||
}
|
||||
|
||||
private val transports = ConcurrentHashMap<String, TransportLayer>()
|
||||
private val seenPackets = Collections.synchronizedMap(
|
||||
object : LinkedHashMap<String, Long>(MAX_SEEN_PACKETS, 0.75f, true) {
|
||||
override fun removeEldestEntry(eldest: MutableMap.MutableEntry<String, Long>?): Boolean {
|
||||
return size > MAX_SEEN_PACKETS
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Register a transport layer to receive bridged packets.
|
||||
@@ -61,12 +74,14 @@ object TransportBridgeService {
|
||||
fun broadcast(sourceId: String, packet: RoutedPacket) {
|
||||
val targets = transports.filterKeys { it != sourceId }
|
||||
if (targets.isEmpty()) return
|
||||
val forwardedPacket = prepareForwardedPacket("broadcast", packet.packet) ?: return
|
||||
val forwarded = packet.copy(packet = forwardedPacket)
|
||||
|
||||
// Log.v(TAG, "Bridging packet type ${packet.packet.type} from $sourceId to ${targets.keys}")
|
||||
|
||||
targets.forEach { (id, layer) ->
|
||||
try {
|
||||
layer.send(packet)
|
||||
layer.send(forwarded)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to bridge packet to $id: ${e.message}")
|
||||
}
|
||||
@@ -79,13 +94,57 @@ object TransportBridgeService {
|
||||
fun sendToPeer(sourceId: String, peerID: String, packet: BitchatPacket) {
|
||||
val targets = transports.filterKeys { it != sourceId }
|
||||
if (targets.isEmpty()) return
|
||||
val forwardedPacket = prepareForwardedPacket("peer:$peerID", packet) ?: return
|
||||
|
||||
targets.forEach { (id, layer) ->
|
||||
try {
|
||||
layer.sendToPeer(peerID, packet)
|
||||
layer.sendToPeer(peerID, forwardedPacket)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to bridge unicast packet to $id: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun prepareForwardedPacket(kind: String, packet: BitchatPacket): BitchatPacket? {
|
||||
if (packet.ttl == 0u.toUByte()) {
|
||||
Log.d(TAG, "Dropping bridged packet type ${packet.type}: TTL expired")
|
||||
return null
|
||||
}
|
||||
|
||||
val key = "$kind:${logicalPacketId(packet)}"
|
||||
val now = System.currentTimeMillis()
|
||||
synchronized(seenPackets) {
|
||||
pruneSeen(now)
|
||||
val previous = seenPackets[key]
|
||||
if (previous != null && now - previous < SEEN_PACKET_TTL_MS) {
|
||||
Log.d(TAG, "Dropping duplicate bridged packet type ${packet.type}")
|
||||
return null
|
||||
}
|
||||
seenPackets[key] = now
|
||||
}
|
||||
|
||||
return packet.copy(ttl = (packet.ttl - 1u).toUByte())
|
||||
}
|
||||
|
||||
private fun pruneSeen(now: Long) {
|
||||
val iterator = seenPackets.entries.iterator()
|
||||
while (iterator.hasNext()) {
|
||||
val entry = iterator.next()
|
||||
if (now - entry.value > SEEN_PACKET_TTL_MS) {
|
||||
iterator.remove()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun logicalPacketId(packet: BitchatPacket): String {
|
||||
val digest = MessageDigest.getInstance("SHA-256")
|
||||
digest.update(packet.type.toByte())
|
||||
digest.update(packet.senderID)
|
||||
packet.recipientID?.let { digest.update(it) }
|
||||
digest.update(packet.timestamp.toString().toByteArray(Charsets.UTF_8))
|
||||
digest.update(packet.payload)
|
||||
packet.route?.forEach { digest.update(it) }
|
||||
packet.signature?.let { digest.update(it) }
|
||||
return digest.digest().toHexString()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ object AppStateStore {
|
||||
// Global de-dup set by message id to avoid duplicate keys in Compose lists
|
||||
private val seenMessageIds = mutableSetOf<String>()
|
||||
private val seenPublicMessageKeys = mutableSetOf<String>()
|
||||
private val peerIdsByTransport = mutableMapOf<String, Set<String>>()
|
||||
// Connected peer IDs (mesh ephemeral IDs)
|
||||
private val _peers = MutableStateFlow<List<String>>(emptyList())
|
||||
val peers: StateFlow<List<String>> = _peers.asStateFlow()
|
||||
@@ -31,7 +32,31 @@ object AppStateStore {
|
||||
val channelMessages: StateFlow<Map<String, List<BitchatMessage>>> = _channelMessages.asStateFlow()
|
||||
|
||||
fun setPeers(ids: List<String>) {
|
||||
_peers.value = ids
|
||||
synchronized(this) {
|
||||
_peers.value = ids.distinct()
|
||||
}
|
||||
}
|
||||
|
||||
fun setTransportPeers(transportId: String, ids: List<String>) {
|
||||
synchronized(this) {
|
||||
peerIdsByTransport[transportId] = ids.toSet()
|
||||
publishTransportPeersLocked()
|
||||
}
|
||||
}
|
||||
|
||||
fun clearTransportPeers(transportId: String) {
|
||||
synchronized(this) {
|
||||
peerIdsByTransport.remove(transportId)
|
||||
publishTransportPeersLocked()
|
||||
}
|
||||
}
|
||||
|
||||
private fun publishTransportPeersLocked() {
|
||||
_peers.value = peerIdsByTransport.values
|
||||
.asSequence()
|
||||
.flatten()
|
||||
.distinct()
|
||||
.toList()
|
||||
}
|
||||
|
||||
fun addPublicMessage(msg: BitchatMessage) {
|
||||
@@ -104,6 +129,7 @@ object AppStateStore {
|
||||
synchronized(this) {
|
||||
seenMessageIds.clear()
|
||||
seenPublicMessageKeys.clear()
|
||||
peerIdsByTransport.clear()
|
||||
_peers.value = emptyList()
|
||||
_publicMessages.value = emptyList()
|
||||
_privateMessages.value = emptyMap()
|
||||
|
||||
@@ -100,6 +100,7 @@ class MeshDelegateHandler(
|
||||
override fun didUpdatePeerList(peers: List<String>) {
|
||||
coroutineScope.launch {
|
||||
blePeers = peers.toSet()
|
||||
try { com.bitchat.android.services.AppStateStore.setTransportPeers("BLE", peers) } catch (_: Exception) { }
|
||||
processPeerUpdate()
|
||||
}
|
||||
}
|
||||
@@ -107,16 +108,15 @@ class MeshDelegateHandler(
|
||||
fun onWifiPeersUpdated(peers: List<String>) {
|
||||
coroutineScope.launch {
|
||||
wifiPeers = peers.toSet()
|
||||
try { com.bitchat.android.services.AppStateStore.setTransportPeers("WIFI", peers) } catch (_: Exception) { }
|
||||
processPeerUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun processPeerUpdate() {
|
||||
// Merge peers from multiple transports
|
||||
val mergedPeers = (blePeers + wifiPeers).toList()
|
||||
|
||||
// Update process-wide state as source of truth
|
||||
try { com.bitchat.android.services.AppStateStore.setPeers(mergedPeers) } catch (_: Exception) { }
|
||||
val mergedPeers = com.bitchat.android.services.AppStateStore.peers.value
|
||||
.ifEmpty { (blePeers + wifiPeers).toList() }
|
||||
|
||||
state.setConnectedPeers(mergedPeers)
|
||||
state.setIsConnected(mergedPeers.isNotEmpty())
|
||||
|
||||
@@ -61,6 +61,8 @@ fun MeshPeerListSheet(
|
||||
val peerNicknames by viewModel.peerNicknames.collectAsStateWithLifecycle()
|
||||
val peerRSSI by viewModel.peerRSSI.collectAsStateWithLifecycle()
|
||||
val selectedLocationChannel by viewModel.selectedLocationChannel.collectAsStateWithLifecycle()
|
||||
val wifiAwareConnected by com.bitchat.android.wifiaware.WifiAwareController.connectedPeers.collectAsStateWithLifecycle()
|
||||
val wifiAwarePeerIDs = remember(wifiAwareConnected) { wifiAwareConnected.keys.toSet() }
|
||||
|
||||
// Bottom sheet state
|
||||
val sheetState = rememberModalBottomSheetState(
|
||||
@@ -163,11 +165,12 @@ fun MeshPeerListSheet(
|
||||
nickname = nickname,
|
||||
colorScheme = colorScheme,
|
||||
selectedPrivatePeer = selectedPrivatePeer,
|
||||
wifiAwarePeerIDs = wifiAwarePeerIDs,
|
||||
viewModel = viewModel,
|
||||
onPrivateChatStart = { peerID ->
|
||||
viewModel.showPrivateChatSheet(peerID)
|
||||
onDismiss()
|
||||
}
|
||||
onPrivateChatStart = { peerID ->
|
||||
viewModel.showPrivateChatSheet(peerID)
|
||||
onDismiss()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -279,6 +282,7 @@ fun PeopleSection(
|
||||
nickname: String,
|
||||
colorScheme: ColorScheme,
|
||||
selectedPrivatePeer: String?,
|
||||
wifiAwarePeerIDs: Set<String> = emptySet(),
|
||||
viewModel: ChatViewModel,
|
||||
onPrivateChatStart: (String) -> Unit
|
||||
) {
|
||||
@@ -416,6 +420,7 @@ fun PeopleSection(
|
||||
peerID = peerID,
|
||||
displayName = displayName,
|
||||
isDirect = isDirectLive,
|
||||
isWifiAware = peerID in wifiAwarePeerIDs,
|
||||
isSelected = peerID == selectedPrivatePeer,
|
||||
isFavorite = isFavorite,
|
||||
isVerified = isVerified,
|
||||
@@ -544,6 +549,7 @@ private fun PeerItem(
|
||||
peerID: String,
|
||||
displayName: String,
|
||||
isDirect: Boolean,
|
||||
isWifiAware: Boolean = false,
|
||||
isSelected: Boolean,
|
||||
isFavorite: Boolean,
|
||||
isVerified: Boolean,
|
||||
@@ -619,10 +625,18 @@ private fun PeerItem(
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
imageVector = if (isDirect) Icons.Outlined.Bluetooth else Icons.Filled.Route,
|
||||
contentDescription = if (isDirect) "Direct Bluetooth" else "Routed",
|
||||
imageVector = when {
|
||||
isWifiAware -> Icons.Filled.Wifi
|
||||
isDirect -> Icons.Outlined.Bluetooth
|
||||
else -> Icons.Filled.Route
|
||||
},
|
||||
contentDescription = when {
|
||||
isWifiAware -> "Direct Wi-Fi Aware"
|
||||
isDirect -> "Direct Bluetooth"
|
||||
else -> "Routed"
|
||||
},
|
||||
modifier = Modifier.size(16.dp),
|
||||
tint = colorScheme.onSurface.copy(alpha = 0.6f)
|
||||
tint = if (isWifiAware) Color(0xFF9C27B0) else colorScheme.onSurface.copy(alpha = 0.6f)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -652,6 +666,16 @@ private fun PeerItem(
|
||||
color = baseColor.copy(alpha = 0.6f)
|
||||
)
|
||||
}
|
||||
|
||||
if (isWifiAware && hasUnreadDM) {
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Wifi,
|
||||
contentDescription = "Direct Wi-Fi Aware",
|
||||
modifier = Modifier.size(13.dp),
|
||||
tint = Color(0xFF9C27B0).copy(alpha = 0.8f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -285,6 +285,9 @@ class DebugSettingsManager private constructor() {
|
||||
DebugPreferenceManager.setBleEnabled(enabled)
|
||||
_bleEnabled.value = enabled
|
||||
addDebugMessage(DebugMessage.SystemMessage(if (enabled) "🟢 BLE enabled" else "🔴 BLE disabled"))
|
||||
try {
|
||||
com.bitchat.android.service.MeshServiceHolder.meshService?.setBleTransportEnabled(enabled)
|
||||
} catch (_: Exception) { }
|
||||
}
|
||||
|
||||
fun setWifiAwareEnabled(enabled: Boolean) {
|
||||
|
||||
@@ -42,10 +42,12 @@ import com.bitchat.android.core.ui.component.sheet.BitchatSheetTopBar
|
||||
import com.bitchat.android.core.ui.component.sheet.BitchatSheetTitle
|
||||
|
||||
@Composable
|
||||
fun MeshTopologySection() {
|
||||
fun MeshTopologySection(localPeerID: String? = null) {
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
val graphService = remember { MeshGraphService.getInstance() }
|
||||
val snapshot by graphService.graphState.collectAsState()
|
||||
val wifiAwareConnected by com.bitchat.android.wifiaware.WifiAwareController.connectedPeers.collectAsState()
|
||||
val wifiAwarePeerIDs = remember(wifiAwareConnected) { wifiAwareConnected.keys.toSet() }
|
||||
|
||||
Surface(shape = RoundedCornerShape(12.dp), color = colorScheme.surfaceVariant.copy(alpha = 0.2f)) {
|
||||
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
@@ -62,6 +64,8 @@ fun MeshTopologySection() {
|
||||
ForceDirectedMeshGraph(
|
||||
nodes = nodes,
|
||||
edges = edges,
|
||||
wifiAwarePeerIDs = wifiAwarePeerIDs,
|
||||
localPeerID = localPeerID,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(300.dp)
|
||||
@@ -221,7 +225,7 @@ fun DebugSettingsSheet(
|
||||
|
||||
// Mesh topology visualization (moved below verbose logging)
|
||||
item {
|
||||
MeshTopologySection()
|
||||
MeshTopologySection(localPeerID = meshService.myPeerID)
|
||||
}
|
||||
|
||||
// GATT controls
|
||||
@@ -307,15 +311,6 @@ fun DebugSettingsSheet(
|
||||
Text("BLE", fontFamily = FontFamily.Monospace, modifier = Modifier.weight(1f))
|
||||
Switch(checked = bleEnabled, onCheckedChange = {
|
||||
manager.setBleEnabled(it)
|
||||
scope.launch {
|
||||
if (it) {
|
||||
if (gattServerEnabled) meshService.connectionManager.startServer()
|
||||
if (gattClientEnabled) meshService.connectionManager.startClient()
|
||||
} else {
|
||||
meshService.connectionManager.stopServer()
|
||||
meshService.connectionManager.stopClient()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
@@ -604,8 +599,8 @@ fun DebugSettingsSheet(
|
||||
Text(if (running) "running" else "stopped", fontFamily = FontFamily.Monospace, fontSize = 12.sp, color = colorScheme.onSurface.copy(alpha = 0.7f))
|
||||
}
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
AssistChip(onClick = { com.bitchat.android.wifiaware.WifiAwareController.startIfPossible() }, label = { Text("Start") })
|
||||
AssistChip(onClick = { com.bitchat.android.wifiaware.WifiAwareController.stop() }, label = { Text("Stop") })
|
||||
AssistChip(onClick = { manager.setWifiAwareEnabled(true) }, label = { Text("Start") })
|
||||
AssistChip(onClick = { manager.setWifiAwareEnabled(false) }, label = { Text("Stop") })
|
||||
AssistChip(onClick = { com.bitchat.android.wifiaware.WifiAwareController.getService()?.sendBroadcastAnnounce() }, label = { Text("Announce") })
|
||||
}
|
||||
Text("Discovered: ${wifiAwareDiscovered.size}", fontFamily = FontFamily.Monospace, fontSize = 12.sp)
|
||||
|
||||
@@ -4,6 +4,11 @@ import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.gestures.detectDragGestures
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Wifi
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
@@ -14,6 +19,7 @@ import androidx.compose.ui.graphics.nativeCanvas
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.bitchat.android.services.meshgraph.MeshGraphService
|
||||
@@ -212,6 +218,8 @@ private class Simulation {
|
||||
fun ForceDirectedMeshGraph(
|
||||
nodes: List<MeshGraphService.GraphNode>,
|
||||
edges: List<MeshGraphService.GraphEdge>,
|
||||
wifiAwarePeerIDs: Set<String> = emptySet(),
|
||||
localPeerID: String? = null,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val density = LocalDensity.current
|
||||
@@ -405,5 +413,46 @@ fun ForceDirectedMeshGraph(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val iconSize = 16.dp
|
||||
val iconSizePx = with(density) { iconSize.toPx() }
|
||||
val halfIconSizePx = iconSizePx / 2f
|
||||
val localID = localPeerID
|
||||
val shouldMarkWifiEdge: (MeshGraphService.GraphEdge) -> Boolean = { edge ->
|
||||
if (localID != null) {
|
||||
(edge.a == localID && edge.b in wifiAwarePeerIDs) ||
|
||||
(edge.b == localID && edge.a in wifiAwarePeerIDs)
|
||||
} else {
|
||||
edge.a in wifiAwarePeerIDs || edge.b in wifiAwarePeerIDs
|
||||
}
|
||||
}
|
||||
|
||||
val wifiEdgeMidpoints = tick.let {
|
||||
simulation.edges.mapNotNull { edge ->
|
||||
val n1 = simulation.nodes[edge.a]
|
||||
val n2 = simulation.nodes[edge.b]
|
||||
if (n1 != null && n2 != null && shouldMarkWifiEdge(edge)) {
|
||||
((n1.x + n2.x) / 2f) to ((n1.y + n2.y) / 2f)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wifiEdgeMidpoints.forEach { (midX, midY) ->
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Wifi,
|
||||
contentDescription = null,
|
||||
tint = Color(0xFF9C27B0).copy(alpha = 0.82f),
|
||||
modifier = Modifier
|
||||
.offset {
|
||||
IntOffset(
|
||||
x = (midX - halfIconSizePx).roundToInt(),
|
||||
y = (midY - halfIconSizePx).roundToInt()
|
||||
)
|
||||
}
|
||||
.size(iconSize)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,29 +22,32 @@ class WifiAwareConnectionTracker(
|
||||
|
||||
// Active resources per peer
|
||||
val peerSockets = ConcurrentHashMap<String, SyncedSocket>()
|
||||
private val socketAliases = ConcurrentHashMap<String, String>()
|
||||
val serverSockets = ConcurrentHashMap<String, ServerSocket>()
|
||||
val networkCallbacks = ConcurrentHashMap<String, ConnectivityManager.NetworkCallback>()
|
||||
|
||||
override fun isConnected(id: String): Boolean {
|
||||
// We consider it connected if we have a client socket to them
|
||||
return peerSockets.containsKey(id)
|
||||
return getSocketForPeer(id) != null
|
||||
}
|
||||
|
||||
override fun disconnect(id: String) {
|
||||
Log.d(TAG, "Disconnecting peer $id")
|
||||
val canonicalId = resolveCanonicalPeerId(id)
|
||||
|
||||
// 1. Close client socket
|
||||
peerSockets.remove(id)?.let {
|
||||
peerSockets.remove(canonicalId)?.let {
|
||||
try { it.close() } catch (e: Exception) { Log.w(TAG, "Error closing socket for $id: ${e.message}") }
|
||||
}
|
||||
socketAliases.entries.removeIf { it.key == id || it.key == canonicalId || it.value == canonicalId }
|
||||
|
||||
// 2. Close server socket
|
||||
serverSockets.remove(id)?.let {
|
||||
serverSockets.remove(canonicalId)?.let {
|
||||
try { it.close() } catch (e: Exception) { Log.w(TAG, "Error closing server socket for $id: ${e.message}") }
|
||||
}
|
||||
|
||||
// Ensure any pending/active network request is explicitly released
|
||||
releaseNetworkRequest(id)
|
||||
releaseNetworkRequest(canonicalId)
|
||||
}
|
||||
|
||||
fun releaseNetworkRequest(id: String) {
|
||||
@@ -73,6 +76,54 @@ class WifiAwareConnectionTracker(
|
||||
removePendingConnection(peerId) // Clear retry state on success
|
||||
}
|
||||
|
||||
fun getSocketForPeer(peerId: String): SyncedSocket? {
|
||||
val canonicalId = resolveCanonicalPeerId(peerId)
|
||||
return peerSockets[canonicalId]
|
||||
}
|
||||
|
||||
fun canonicalPeerId(peerId: String): String = resolveCanonicalPeerId(peerId)
|
||||
|
||||
fun rebindPeerId(previousPeerId: String, resolvedPeerId: String, socket: SyncedSocket): String {
|
||||
if (previousPeerId == resolvedPeerId) {
|
||||
peerSockets[resolvedPeerId] = socket
|
||||
return resolvedPeerId
|
||||
}
|
||||
|
||||
val previousCanonical = resolveCanonicalPeerId(previousPeerId)
|
||||
val existing = peerSockets[previousCanonical]
|
||||
if (existing === socket) {
|
||||
peerSockets.remove(previousCanonical)
|
||||
}
|
||||
|
||||
peerSockets[resolvedPeerId]?.let { current ->
|
||||
if (current !== socket) {
|
||||
try { current.close() } catch (_: Exception) { }
|
||||
}
|
||||
}
|
||||
peerSockets[resolvedPeerId] = socket
|
||||
serverSockets.remove(previousCanonical)?.let { serverSockets[resolvedPeerId] = it }
|
||||
networkCallbacks.remove(previousCanonical)?.let { networkCallbacks[resolvedPeerId] = it }
|
||||
socketAliases[previousPeerId] = resolvedPeerId
|
||||
if (previousCanonical != previousPeerId) {
|
||||
socketAliases[previousCanonical] = resolvedPeerId
|
||||
}
|
||||
removePendingConnection(previousPeerId)
|
||||
removePendingConnection(resolvedPeerId)
|
||||
|
||||
Log.i(TAG, "Rebound Wi-Fi Aware socket ${previousPeerId.take(8)} -> ${resolvedPeerId.take(8)}")
|
||||
return resolvedPeerId
|
||||
}
|
||||
|
||||
private fun resolveCanonicalPeerId(peerId: String): String {
|
||||
var current = peerId
|
||||
val visited = mutableSetOf<String>()
|
||||
while (visited.add(current)) {
|
||||
val next = socketAliases[current] ?: return current
|
||||
current = next
|
||||
}
|
||||
return current
|
||||
}
|
||||
|
||||
fun addServerSocket(peerId: String, socket: ServerSocket) {
|
||||
serverSockets[peerId] = socket
|
||||
}
|
||||
@@ -96,6 +147,12 @@ class WifiAwareConnectionTracker(
|
||||
peerSockets.keys.forEach { pid ->
|
||||
appendLine(" - $pid (Socket)")
|
||||
}
|
||||
if (socketAliases.isNotEmpty()) {
|
||||
appendLine("Socket aliases:")
|
||||
socketAliases.forEach { (alias, canonical) ->
|
||||
appendLine(" - $alias -> $canonical")
|
||||
}
|
||||
}
|
||||
appendLine("Server Sockets: ${serverSockets.size}")
|
||||
serverSockets.keys.forEach { pid ->
|
||||
appendLine(" - $pid (Listening)")
|
||||
|
||||
@@ -84,6 +84,12 @@ object WifiAwareController {
|
||||
try { com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().addDebugMessage(com.bitchat.android.ui.debug.DebugMessage.SystemMessage("Wi‑Fi Aware not supported on this device (requires Android 10+)")) } catch (_: Exception) {}
|
||||
return
|
||||
}
|
||||
val awareManager = ctx.getSystemService(android.net.wifi.aware.WifiAwareManager::class.java)
|
||||
if (awareManager == null || !awareManager.isAvailable) {
|
||||
Log.w(TAG, "Wi-Fi Aware is not currently available; not starting")
|
||||
try { com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().addDebugMessage(com.bitchat.android.ui.debug.DebugMessage.SystemMessage("Wi‑Fi Aware is not available on this device right now")) } catch (_: Exception) {}
|
||||
return
|
||||
}
|
||||
|
||||
// Check system location setting: WifiAwareManager.attach() throws SecurityException if disabled
|
||||
val lm = ctx.getSystemService(Context.LOCATION_SERVICE) as? android.location.LocationManager
|
||||
@@ -111,11 +117,18 @@ object WifiAwareController {
|
||||
}
|
||||
}
|
||||
try {
|
||||
service = WifiAwareMeshService(ctx).also {
|
||||
Log.i(TAG, "Instantiating WifiAwareMeshService...")
|
||||
it.startServices()
|
||||
Log.i(TAG, "Instantiating WifiAwareMeshService...")
|
||||
val startedService = WifiAwareMeshService(ctx)
|
||||
startedService.startServices()
|
||||
if (startedService.isRunning()) {
|
||||
service = startedService
|
||||
_running.value = true
|
||||
try { com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().addDebugMessage(com.bitchat.android.ui.debug.DebugMessage.SystemMessage("Wi‑Fi Aware started")) } catch (_: Exception) {}
|
||||
} else {
|
||||
try { startedService.stopServices() } catch (_: Exception) { }
|
||||
service = null
|
||||
_running.value = false
|
||||
try { com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().addDebugMessage(com.bitchat.android.ui.debug.DebugMessage.SystemMessage("Wi‑Fi Aware did not start")) } catch (_: Exception) {}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, "Failed to start WifiAwareMeshService", e)
|
||||
@@ -128,8 +141,30 @@ object WifiAwareController {
|
||||
try { service?.stopServices() } catch (_: Exception) { }
|
||||
service = null
|
||||
_running.value = false
|
||||
try { com.bitchat.android.services.AppStateStore.clearTransportPeers("WIFI") } catch (_: Exception) { }
|
||||
_connectedPeers.value = emptyMap()
|
||||
_knownPeers.value = emptyMap()
|
||||
_discoveredPeers.value = emptySet()
|
||||
try { com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().addDebugMessage(com.bitchat.android.ui.debug.DebugMessage.SystemMessage("Wi‑Fi Aware stopped")) } catch (_: Exception) {}
|
||||
}
|
||||
|
||||
internal fun onServiceStopped(stoppedService: WifiAwareMeshService) {
|
||||
if (service === stoppedService) {
|
||||
service = null
|
||||
_running.value = false
|
||||
try { com.bitchat.android.services.AppStateStore.clearTransportPeers("WIFI") } catch (_: Exception) { }
|
||||
_connectedPeers.value = emptyMap()
|
||||
_knownPeers.value = emptyMap()
|
||||
_discoveredPeers.value = emptySet()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun restartIfStillEnabled(delayMs: Long = 0L) {
|
||||
scope.launch {
|
||||
if (delayMs > 0L) delay(delayMs)
|
||||
if (_enabled.value) startIfPossible()
|
||||
}
|
||||
}
|
||||
|
||||
fun getService(): WifiAwareMeshService? = service
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ import com.bitchat.android.model.BitchatFilePacket
|
||||
import com.bitchat.android.model.BitchatMessage
|
||||
import com.bitchat.android.model.RoutedPacket
|
||||
import com.bitchat.android.protocol.BitchatPacket
|
||||
import com.bitchat.android.protocol.MessageType
|
||||
import com.bitchat.android.protocol.SpecialRecipients
|
||||
import com.bitchat.android.service.TransportBridgeService
|
||||
import com.bitchat.android.sync.GossipSyncManager
|
||||
import com.bitchat.android.util.toHexString
|
||||
@@ -102,6 +104,8 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
||||
// Timestamp dedupe
|
||||
private val lastTimestamps = ConcurrentHashMap<String, ULong>()
|
||||
|
||||
fun isRunning(): Boolean = isActive
|
||||
|
||||
init {
|
||||
// Ensure BluetoothMeshService is initialized so we share its GossipSyncManager
|
||||
// This avoids race conditions and ensures a single gossip source/delegate
|
||||
@@ -198,8 +202,27 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
||||
*/
|
||||
private fun broadcastPacket(routed: RoutedPacket) {
|
||||
Log.d(TAG, "TX: packet type=${routed.packet.type} broadcast (ttl=${routed.packet.ttl})")
|
||||
|
||||
val packet = routed.packet
|
||||
if (packet.senderID.toHexString() == myPeerID && !packet.route.isNullOrEmpty()) {
|
||||
val firstHop = packet.route!![0].toHexString()
|
||||
if (sendPacketToPeer(firstHop, packet)) {
|
||||
Log.d(TAG, "TX: source-routed packet sent only to first Wi-Fi hop ${firstHop.take(8)}")
|
||||
return
|
||||
}
|
||||
Log.w(TAG, "TX: first Wi-Fi source-route hop ${firstHop.take(8)} unavailable; falling back to broadcast")
|
||||
}
|
||||
|
||||
val recipientId = packet.recipientID?.toHexString()
|
||||
if (recipientId != null && !packet.recipientID.contentEquals(SpecialRecipients.BROADCAST)) {
|
||||
if (sendPacketToPeer(recipientId, packet)) {
|
||||
Log.d(TAG, "TX: addressed packet sent directly to Wi-Fi peer ${recipientId.take(8)}")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Wi-Fi Aware uses full packets; no fragmentation
|
||||
val data = routed.packet.toBinaryData() ?: return
|
||||
val data = packet.toBinaryData() ?: return
|
||||
serviceScope.launch { broadcastRaw(data) }
|
||||
}
|
||||
|
||||
@@ -211,15 +234,15 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
||||
/**
|
||||
* Send packet to connected peer.
|
||||
*/
|
||||
private fun sendPacketToPeer(peerID: String, packet: BitchatPacket) {
|
||||
private fun sendPacketToPeer(peerID: String, packet: BitchatPacket): Boolean {
|
||||
// Wi-Fi Aware uses full packets; no fragmentation
|
||||
val data = packet.toBinaryData() ?: return
|
||||
val data = packet.toBinaryData() ?: return false
|
||||
val sock = connectionTracker.getSocketForPeer(peerID)
|
||||
if (sock == null) {
|
||||
Log.w(TAG, "TX: no socket for ${peerID.take(8)}")
|
||||
return false
|
||||
}
|
||||
serviceScope.launch {
|
||||
val sock = connectionTracker.peerSockets[peerID]
|
||||
if (sock == null) {
|
||||
Log.w(TAG, "TX: no socket for ${peerID.take(8)}")
|
||||
return@launch
|
||||
}
|
||||
try {
|
||||
sock.write(data)
|
||||
Log.d(TAG, "TX: packet type=${packet.type} to ${peerID.take(8)} (bytes=${data.size})")
|
||||
@@ -227,6 +250,7 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
||||
Log.e(TAG, "TX: write to ${peerID.take(8)} failed: ${e.message}")
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@@ -245,10 +269,19 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
||||
])
|
||||
override fun startServices() {
|
||||
if (isActive) return
|
||||
if (!com.bitchat.android.wifiaware.WifiAwareController.enabled.value) {
|
||||
Log.i(TAG, "Wi-Fi Aware transport disabled by debug settings; not starting")
|
||||
return
|
||||
}
|
||||
val manager = awareManager
|
||||
if (manager == null || !manager.isAvailable) {
|
||||
Log.w(TAG, "Wi-Fi Aware manager unavailable; not starting")
|
||||
return
|
||||
}
|
||||
isActive = true
|
||||
Log.i(TAG, "Starting Wi-Fi Aware mesh with peer ID: $myPeerID")
|
||||
|
||||
awareManager?.attach(object : AttachCallback() {
|
||||
manager.attach(object : AttachCallback() {
|
||||
@SuppressLint("MissingPermission")
|
||||
@RequiresPermission(allOf = [
|
||||
Manifest.permission.ACCESS_FINE_LOCATION,
|
||||
@@ -302,10 +335,11 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
||||
override fun onSessionTerminated() {
|
||||
Log.e(TAG, "PUBLISH: onSessionTerminated()")
|
||||
publishSession = null
|
||||
if (isActive) {
|
||||
Log.i(TAG, "PUBLISH: Attempting to restart publish session...")
|
||||
// Delay and check if we need to restart services entirely
|
||||
serviceScope.launch { delay(2000); if (isActive) startServices() }
|
||||
val shouldRestart = isActive && com.bitchat.android.wifiaware.WifiAwareController.enabled.value
|
||||
handleUnexpectedStop()
|
||||
if (shouldRestart) {
|
||||
Log.i(TAG, "PUBLISH: Scheduling Wi-Fi Aware restart")
|
||||
com.bitchat.android.wifiaware.WifiAwareController.restartIfStillEnabled(2000)
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -352,9 +386,11 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
||||
override fun onSessionTerminated() {
|
||||
Log.e(TAG, "SUBSCRIBE: onSessionTerminated()")
|
||||
subscribeSession = null
|
||||
if (isActive) {
|
||||
Log.i(TAG, "SUBSCRIBE: Attempting to restart subscribe session...")
|
||||
serviceScope.launch { delay(2000); if (isActive) startServices() }
|
||||
val shouldRestart = isActive && com.bitchat.android.wifiaware.WifiAwareController.enabled.value
|
||||
handleUnexpectedStop()
|
||||
if (shouldRestart) {
|
||||
Log.i(TAG, "SUBSCRIBE: Scheduling Wi-Fi Aware restart")
|
||||
com.bitchat.android.wifiaware.WifiAwareController.restartIfStillEnabled(2000)
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -363,13 +399,17 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
||||
}
|
||||
override fun onAttachFailed() {
|
||||
Log.e(TAG, "Wi-Fi Aware attach failed")
|
||||
handleUnexpectedStop()
|
||||
}
|
||||
|
||||
override fun onAwareSessionTerminated() {
|
||||
Log.e(TAG, "Aware Session Terminated unexpectedly")
|
||||
wifiAwareSession = null
|
||||
isActive = false
|
||||
if (com.bitchat.android.wifiaware.WifiAwareController.enabled.value) { serviceScope.launch { delay(3000); com.bitchat.android.wifiaware.WifiAwareController.startIfPossible() } }
|
||||
val shouldRestart = com.bitchat.android.wifiaware.WifiAwareController.enabled.value
|
||||
handleUnexpectedStop()
|
||||
if (shouldRestart) {
|
||||
com.bitchat.android.wifiaware.WifiAwareController.restartIfStillEnabled(3000)
|
||||
}
|
||||
}
|
||||
}, Handler(Looper.getMainLooper()))
|
||||
|
||||
@@ -385,14 +425,17 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
||||
* Stops the Wi-Fi Aware mesh services and cleans up sockets and sessions.
|
||||
*/
|
||||
override fun stopServices() {
|
||||
if (!isActive) return
|
||||
val wasActive = isActive
|
||||
isActive = false
|
||||
Log.i(TAG, "Stopping Wi-Fi Aware mesh")
|
||||
|
||||
// Unregister from bridge
|
||||
TransportBridgeService.unregister("WIFI")
|
||||
try { com.bitchat.android.services.AppStateStore.clearTransportPeers("WIFI") } catch (_: Exception) { }
|
||||
|
||||
meshCore.sendLeaveAnnouncement()
|
||||
if (wasActive) {
|
||||
meshCore.sendLeaveAnnouncement()
|
||||
}
|
||||
|
||||
serviceScope.launch {
|
||||
delay(200)
|
||||
@@ -408,6 +451,31 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
||||
|
||||
meshCore.shutdown()
|
||||
|
||||
com.bitchat.android.wifiaware.WifiAwareController.onServiceStopped(this@WifiAwareMeshService)
|
||||
serviceScope.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleUnexpectedStop() {
|
||||
if (!isActive) {
|
||||
com.bitchat.android.wifiaware.WifiAwareController.onServiceStopped(this)
|
||||
return
|
||||
}
|
||||
isActive = false
|
||||
TransportBridgeService.unregister("WIFI")
|
||||
try { com.bitchat.android.services.AppStateStore.clearTransportPeers("WIFI") } catch (_: Exception) { }
|
||||
serviceScope.launch {
|
||||
try { meshCore.stopCore() } catch (_: Exception) { }
|
||||
try { connectionTracker.stop() } catch (_: Exception) { }
|
||||
try { publishSession?.close() } catch (_: Exception) { }
|
||||
try { subscribeSession?.close() } catch (_: Exception) { }
|
||||
try { wifiAwareSession?.close() } catch (_: Exception) { }
|
||||
publishSession = null
|
||||
subscribeSession = null
|
||||
wifiAwareSession = null
|
||||
handleToPeerId.clear()
|
||||
try { meshCore.shutdown() } catch (_: Exception) { }
|
||||
com.bitchat.android.wifiaware.WifiAwareController.onServiceStopped(this@WifiAwareMeshService)
|
||||
serviceScope.cancel()
|
||||
}
|
||||
}
|
||||
@@ -748,6 +816,7 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
||||
* @param initialLogicalPeerId Temporary identifier before peer ID resolution
|
||||
*/
|
||||
private fun listenToPeer(socket: SyncedSocket, initialLogicalPeerId: String) {
|
||||
var logicalPeerId = initialLogicalPeerId
|
||||
while (isActive) {
|
||||
val raw = socket.read() ?: break
|
||||
|
||||
@@ -759,6 +828,22 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
||||
val pkt = BitchatPacket.fromBinaryData(raw) ?: continue
|
||||
|
||||
val senderPeerHex = pkt.senderID?.toHexString()?.take(16) ?: continue
|
||||
|
||||
if (pkt.type == MessageType.ANNOUNCE.value && pkt.ttl >= MAX_TTL && senderPeerHex != logicalPeerId) {
|
||||
val previousPeerId = logicalPeerId
|
||||
logicalPeerId = connectionTracker.rebindPeerId(previousPeerId, senderPeerHex, socket)
|
||||
handleToPeerId.forEach { (handle, peerId) ->
|
||||
if (peerId == previousPeerId) {
|
||||
handleToPeerId[handle] = senderPeerHex
|
||||
}
|
||||
}
|
||||
discoveredTimestamps.remove(previousPeerId)
|
||||
discoveredTimestamps[senderPeerHex] = System.currentTimeMillis()
|
||||
try { meshCore.setDirectConnection(previousPeerId, false) } catch (_: Exception) { }
|
||||
try { meshCore.removePeer(previousPeerId) } catch (_: Exception) { }
|
||||
try { meshCore.setDirectConnection(senderPeerHex, true) } catch (_: Exception) { }
|
||||
Log.i(TAG, "RX: rebound Wi-Fi direct peer ${previousPeerId.take(8)} -> ${senderPeerHex.take(8)}")
|
||||
}
|
||||
|
||||
// Deduplicate based on timestamp + sender (standard flood fill protection)
|
||||
val ts = pkt.timestamp
|
||||
@@ -769,15 +854,13 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
||||
// Route the packet:
|
||||
// - peerID = Originator (who signed it)
|
||||
// - 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)} via ${initialLogicalPeerId.take(8)} (bytes=${raw.size})")
|
||||
meshCore.processIncoming(pkt, senderPeerHex, initialLogicalPeerId)
|
||||
Log.d(TAG, "RX: packet type=${pkt.type} from ${senderPeerHex.take(8)} via ${logicalPeerId.take(8)} (bytes=${raw.size})")
|
||||
meshCore.processIncoming(pkt, senderPeerHex, logicalPeerId)
|
||||
}
|
||||
|
||||
// Breaking out of the loop means the socket is dead or service is stopping.
|
||||
Log.i(TAG, "Socket loop terminated for ${initialLogicalPeerId.take(8)} removing peer.")
|
||||
handlePeerDisconnection(initialLogicalPeerId, socket)
|
||||
Log.i(TAG, "Socket loop terminated for ${logicalPeerId.take(8)} removing peer.")
|
||||
handlePeerDisconnection(logicalPeerId, socket)
|
||||
socket.close()
|
||||
}
|
||||
|
||||
@@ -788,7 +871,11 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
||||
connectionTracker.releaseNetworkRequest(peerId)
|
||||
|
||||
if (!connectionTracker.isConnected(peerId)) {
|
||||
meshCore.removePeer(peerId)
|
||||
val canonicalPeerId = connectionTracker.canonicalPeerId(peerId)
|
||||
meshCore.removePeer(canonicalPeerId)
|
||||
if (canonicalPeerId != peerId) {
|
||||
meshCore.removePeer(peerId)
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "Network failure ignored for $peerId - another socket is active")
|
||||
}
|
||||
@@ -798,16 +885,23 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
||||
private fun handlePeerDisconnection(initialId: String, socket: SyncedSocket? = null) {
|
||||
serviceScope.launch {
|
||||
// Check if this socket is the current active one before nuking the session
|
||||
val currentSocket = connectionTracker.peerSockets[initialId]
|
||||
val currentSocket = connectionTracker.getSocketForPeer(initialId)
|
||||
val canonicalPeerId = connectionTracker.canonicalPeerId(initialId)
|
||||
if (currentSocket === socket) {
|
||||
Log.d(TAG, "Cleaning up peer: $initialId (active socket)")
|
||||
Log.d(TAG, "Cleaning up peer: $canonicalPeerId (active socket)")
|
||||
connectionTracker.disconnect(initialId)
|
||||
meshCore.removePeer(initialId)
|
||||
meshCore.removePeer(canonicalPeerId)
|
||||
if (canonicalPeerId != initialId) {
|
||||
meshCore.removePeer(initialId)
|
||||
}
|
||||
} else if (socket == null && currentSocket == null) {
|
||||
// Fallback: If we don't have a specific socket context but we are already disconnected, ensure cleanup
|
||||
Log.d(TAG, "Cleaning up peer: $initialId (no active socket)")
|
||||
connectionTracker.disconnect(initialId)
|
||||
meshCore.removePeer(initialId)
|
||||
meshCore.removePeer(canonicalPeerId)
|
||||
if (canonicalPeerId != initialId) {
|
||||
meshCore.removePeer(initialId)
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "Ignored disconnection for $initialId - socket replaced or inactive")
|
||||
// Do not remove peer/session, as a new socket has likely taken over
|
||||
@@ -1036,11 +1130,11 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
||||
override fun broadcastPacket(routed: RoutedPacket) {
|
||||
this@WifiAwareMeshService.broadcastPacket(routed)
|
||||
}
|
||||
override fun sendPacketToPeer(peerID: String, packet: BitchatPacket) {
|
||||
this@WifiAwareMeshService.sendPacketToPeer(peerID, packet)
|
||||
override fun sendPacketToPeer(peerID: String, packet: BitchatPacket): Boolean {
|
||||
return this@WifiAwareMeshService.sendPacketToPeer(peerID, packet)
|
||||
}
|
||||
override fun getDeviceAddressForPeer(peerID: String): String? {
|
||||
return connectionTracker.peerSockets[peerID]?.let { resolveScopedAddress(it.rawSocket) }
|
||||
return connectionTracker.getSocketForPeer(peerID)?.let { resolveScopedAddress(it.rawSocket) }
|
||||
}
|
||||
|
||||
override fun getDeviceAddressToPeerMapping(): Map<String, String> {
|
||||
|
||||
@@ -55,4 +55,16 @@ class AppStateStoreTest {
|
||||
|
||||
assertEquals(listOf(first, second), AppStateStore.publicMessages.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `peer list merges transport updates instead of overwriting`() {
|
||||
AppStateStore.setTransportPeers("WIFI", listOf("wifi-peer"))
|
||||
AppStateStore.setTransportPeers("BLE", emptyList())
|
||||
|
||||
assertEquals(listOf("wifi-peer"), AppStateStore.peers.value)
|
||||
|
||||
AppStateStore.setTransportPeers("BLE", listOf("ble-peer"))
|
||||
|
||||
assertEquals(listOf("wifi-peer", "ble-peer"), AppStateStore.peers.value)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user