diff --git a/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt b/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt index bc920f9b..76811540 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt @@ -4,20 +4,18 @@ import android.content.Context import android.util.Log import com.bitchat.android.crypto.EncryptionService import com.bitchat.android.model.BitchatMessage -import com.bitchat.android.protocol.MessagePadding import com.bitchat.android.model.RoutedPacket -import com.bitchat.android.model.IdentityAnnouncement import com.bitchat.android.protocol.BitchatPacket -import com.bitchat.android.protocol.MessageType -import com.bitchat.android.protocol.SpecialRecipients -import com.bitchat.android.model.RequestSyncPacket import com.bitchat.android.sync.GossipSyncManager import com.bitchat.android.util.toHexString import com.bitchat.android.service.TransportBridgeService -import kotlinx.coroutines.* -import java.util.* -import kotlin.math.sign -import kotlin.random.Random +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch /** * Bluetooth mesh service - REFACTORED to use component-based architecture @@ -32,27 +30,21 @@ import kotlin.random.Random * - BluetoothConnectionManager: BLE connections and GATT operations * - PacketProcessor: Incoming packet routing */ -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 } } - +class BluetoothMeshService(private val context: Context) : MeshService, TransportBridgeService.TransportLayer { companion object { private const val TAG = "BluetoothMeshService" private val MAX_TTL: UByte = com.bitchat.android.util.AppConstants.MESSAGE_TTL_HOPS } - // Core components - each handling specific responsibilities + // Core components private val encryptionService = EncryptionService(context) // My peer identification - derived from persisted Noise identity fingerprint (first 16 hex chars) - val myPeerID: String = encryptionService.getIdentityFingerprint().take(16) - private val peerManager = PeerManager() - private val fragmentManager = FragmentManager() - private val securityManager = SecurityManager(encryptionService, myPeerID) - private val storeForwardManager = StoreForwardManager() - private val messageHandler = MessageHandler(myPeerID, context.applicationContext) - internal val connectionManager = BluetoothConnectionManager(context, myPeerID, fragmentManager) // Made internal for access - private val packetProcessor = PacketProcessor(myPeerID) - private lateinit var gossipSyncManager: GossipSyncManager + override val myPeerID: String = encryptionService.getIdentityFingerprint().take(16) + private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + private val bleTransport = BleMeshTransport() + private lateinit var meshCore: MeshCore + internal lateinit var connectionManager: BluetoothConnectionManager // Service-level notification manager for background (no-UI) DMs private val serviceNotificationManager = com.bitchat.android.ui.NotificationManager( context.applicationContext, @@ -64,24 +56,27 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic private var isActive = false // Delegate for message callbacks (maintains same interface) - var delegate: BluetoothMeshDelegate? = null - - // Coroutines - private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + override var delegate: BluetoothMeshDelegate? = null + set(value) { + field = value + if (::meshCore.isInitialized) { + meshCore.delegate = value + } + } // Tracks whether this instance has been terminated via stopServices() private var terminated = false init { Log.i(TAG, "Initializing BluetoothMeshService for peer=$myPeerID") - setupDelegates() - messageHandler.packetProcessor = packetProcessor - //startPeriodicDebugLogging() - - // Initialize sync manager (needs serviceScope) - gossipSyncManager = GossipSyncManager( - myPeerID = myPeerID, + meshCore = MeshCore( + context = context.applicationContext, scope = serviceScope, - configProvider = object : GossipSyncManager.ConfigProvider { + transport = bleTransport, + encryptionService = encryptionService, + myPeerID = myPeerID, + maxTtl = MAX_TTL, + sharedGossipManager = null, + gossipConfigProvider = object : GossipSyncManager.ConfigProvider { override fun seenCapacity(): Int = try { com.bitchat.android.ui.debug.DebugPreferenceManager.getSeenPacketCapacity(500) } catch (_: Exception) { 500 } @@ -93,32 +88,76 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic override fun gcsTargetFpr(): Double = try { com.bitchat.android.ui.debug.DebugPreferenceManager.getGcsFprPercent(1.0) / 100.0 } catch (_: Exception) { 0.01 } - } + }, + hooks = MeshCore.Hooks( + onMessageReceived = { message -> handleMessageReceived(message) }, + onPeerIdBindingUpdated = { newPeerID, _, publicKey, previousPeerID -> + try { + com.bitchat.android.favorites.FavoritesPersistenceService.shared + .findNostrPubkey(publicKey) + ?.let { npub -> + com.bitchat.android.favorites.FavoritesPersistenceService.shared + .updateNostrPublicKeyForPeerID(newPeerID, npub) + } + } catch (_: Exception) { } + Log.d(TAG, "Updated peer ID binding: $newPeerID (was: $previousPeerID) publicKey=${publicKey.toHexString().take(16)}...") + }, + onAnnounceProcessed = { routed, _ -> + val deviceAddress = routed.relayAddress + val pid = routed.peerID + if (deviceAddress != null && pid != null) { + if (!connectionManager.hasSeenFirstAnnounce(deviceAddress)) { + connectionManager.addressPeerMap[deviceAddress] = pid + connectionManager.noteAnnounceReceived(deviceAddress) + Log.d(TAG, "Mapped device $deviceAddress to peer $pid on FIRST-ANNOUNCE for this connection") + try { meshCore.setDirectConnection(pid, true) } catch (_: Exception) { } + try { meshCore.gossipSyncManager.scheduleInitialSyncToPeer(pid, 1_000) } catch (_: Exception) { } + } + } + }, + readReceiptInterceptor = { messageId, recipientPeerId -> + val geo = runCatching { com.bitchat.android.services.MessageRouter.tryGetInstance() }.getOrNull() + val isGeoAlias = try { + val map = com.bitchat.android.nostr.GeohashAliasRegistry.snapshot() + map.containsKey(recipientPeerId) + } catch (_: Exception) { false } + if (isGeoAlias && geo != null) { + geo.sendReadReceipt(com.bitchat.android.model.ReadReceipt(messageId), recipientPeerId) + true + } else { + val seenStore = try { + com.bitchat.android.services.SeenMessageStore.getInstance(context.applicationContext) + } catch (_: Exception) { null } + if (seenStore?.hasRead(messageId) == true) { + Log.d(TAG, "Skipping read receipt for $messageId - already marked read") + true + } else { + false + } + } + }, + onReadReceiptSent = { messageId -> + try { + com.bitchat.android.services.SeenMessageStore.getInstance(context.applicationContext) + .markRead(messageId) + } catch (_: Exception) { } + }, + announcementNicknameProvider = { + try { com.bitchat.android.services.NicknameProvider.getNickname(context, myPeerID) } catch (_: Exception) { null } + } + ) ) - - // Register as shared instance for Wi-Fi Aware transport - com.bitchat.android.service.MeshServiceHolder.setGossipManager(gossipSyncManager) - // Wire sync manager delegate - gossipSyncManager.delegate = object : GossipSyncManager.Delegate { - override fun sendPacket(packet: BitchatPacket) { - dispatchGlobal(RoutedPacket(packet)) - } - 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) - } - override fun signPacketForBroadcast(packet: BitchatPacket): BitchatPacket { - return signPacketBeforeBroadcast(packet) - } - } - Log.d(TAG, "Delegates set up; GossipSyncManager initialized") - + connectionManager = BluetoothConnectionManager(context, myPeerID, meshCore.fragmentManager) + bleTransport.connectionManager = connectionManager + setupConnectionManagerDelegate() + + // Register as shared instance for Wi-Fi Aware transport + com.bitchat.android.service.MeshServiceHolder.setGossipManager(meshCore.gossipSyncManager) + + Log.d(TAG, "MeshCore initialized") + //startPeriodicDebugLogging() + // Register with cross-layer transport bridge TransportBridgeService.register("BLE", this) } @@ -127,17 +166,7 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic 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) + meshCore.sendFromBridge(packet) } /** @@ -161,402 +190,15 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic } } - /** - * Send broadcast announcement every 30 seconds - */ - private fun sendPeriodicBroadcastAnnounce() { - serviceScope.launch { - Log.d(TAG, "Starting periodic announce loop") - while (isActive) { - try { - delay(30000) // 30 seconds - sendBroadcastAnnounce() - } catch (e: Exception) { - Log.e(TAG, "Error in periodic broadcast announce: ${e.message}") - } - } - Log.d(TAG, "Periodic announce loop ended (isActive=$isActive)") - } - } - - /** - * Setup delegate connections between components - */ - private fun setupDelegates() { - Log.d(TAG, "Setting up component delegates") - // Provide nickname resolver to BLE broadcaster for detailed logs + private fun setupConnectionManagerDelegate() { try { - connectionManager.setNicknameResolver { pid -> peerManager.getPeerNickname(pid) } + connectionManager.setNicknameResolver { pid -> meshCore.getPeerNickname(pid) } } catch (_: Exception) { } - // PeerManager delegates to main mesh service delegate - peerManager.delegate = object : PeerManagerDelegate { - override fun onPeerListUpdated(peerIDs: List) { - // Then notify UI delegate if attached - delegate?.didUpdatePeerList(peerIDs) - } - override fun onPeerRemoved(peerID: String) { - try { gossipSyncManager.removeAnnouncementForPeer(peerID) } catch (_: Exception) { } - // Also drop any Noise session state for this peer when they go offline - try { - encryptionService.removePeer(peerID) - Log.d(TAG, "Removed Noise session for offline peer $peerID") - } catch (e: Exception) { - Log.w(TAG, "Failed to remove Noise session for $peerID: ${e.message}") - } - } - } - - // SecurityManager delegate for key exchange notifications - securityManager.delegate = object : SecurityManagerDelegate { - override fun onKeyExchangeCompleted(peerID: String, peerPublicKeyData: ByteArray) { - // Send announcement and cached messages after key exchange - serviceScope.launch { - Log.d(TAG, "Key exchange completed with $peerID; sending follow-ups") - delay(100) - sendAnnouncementToPeer(peerID) - - delay(1000) - storeForwardManager.sendCachedMessages(peerID) - } - } - - override fun sendHandshakeResponse(peerID: String, response: ByteArray) { - // Send Noise handshake response - val responsePacket = BitchatPacket( - version = 1u, - type = MessageType.NOISE_HANDSHAKE.value, - senderID = hexStringToByteArray(myPeerID), - recipientID = hexStringToByteArray(peerID), - timestamp = System.currentTimeMillis().toULong(), - payload = response, - ttl = MAX_TTL - ) - // Sign the handshake response - val signedPacket = signPacketBeforeBroadcast(responsePacket) - dispatchGlobal(RoutedPacket(signedPacket)) - Log.d(TAG, "Sent Noise handshake response to $peerID (${response.size} bytes)") - } - - override fun getPeerInfo(peerID: String): PeerInfo? { - return peerManager.getPeerInfo(peerID) - } - } - - // StoreForwardManager delegates - storeForwardManager.delegate = object : StoreForwardManagerDelegate { - override fun isFavorite(peerID: String): Boolean { - return delegate?.isFavorite(peerID) ?: false - } - - override fun isPeerOnline(peerID: String): Boolean { - return peerManager.isPeerActive(peerID) - } - - override fun sendPacket(packet: BitchatPacket) { - dispatchGlobal(RoutedPacket(packet)) - } - } - - // MessageHandler delegates - messageHandler.delegate = object : MessageHandlerDelegate { - // Peer management - override fun addOrUpdatePeer(peerID: String, nickname: String): Boolean { - return peerManager.addOrUpdatePeer(peerID, nickname) - } - - override fun removePeer(peerID: String) { - peerManager.removePeer(peerID) - } - - override fun updatePeerNickname(peerID: String, nickname: String) { - peerManager.addOrUpdatePeer(peerID, nickname) - } - - override fun getPeerNickname(peerID: String): String? { - return peerManager.getPeerNickname(peerID) - } - - override fun getNetworkSize(): Int { - return peerManager.getActivePeerCount() - } - - override fun getMyNickname(): String? { - return delegate?.getNickname() - } - - override fun getPeerInfo(peerID: String): PeerInfo? { - return peerManager.getPeerInfo(peerID) - } - - override fun updatePeerInfo(peerID: String, nickname: String, noisePublicKey: ByteArray, signingPublicKey: ByteArray, isVerified: Boolean): Boolean { - return peerManager.updatePeerInfo(peerID, nickname, noisePublicKey, signingPublicKey, isVerified) - } - - // Packet operations - override fun sendPacket(packet: BitchatPacket) { - // Sign the packet before broadcasting - val signedPacket = signPacketBeforeBroadcast(packet) - val routed = RoutedPacket(signedPacket) - dispatchGlobal(routed) - } - - override fun relayPacket(routed: RoutedPacket) { - dispatchGlobal(routed) - } - - override fun getBroadcastRecipient(): ByteArray { - return SpecialRecipients.BROADCAST - } - - // Cryptographic operations - override fun verifySignature(packet: BitchatPacket, peerID: String): Boolean { - return securityManager.verifySignature(packet, peerID) - } - - override fun encryptForPeer(data: ByteArray, recipientPeerID: String): ByteArray? { - return securityManager.encryptForPeer(data, recipientPeerID) - } - - override fun decryptFromPeer(encryptedData: ByteArray, senderPeerID: String): ByteArray? { - return securityManager.decryptFromPeer(encryptedData, senderPeerID) - } - - override fun verifyEd25519Signature(signature: ByteArray, data: ByteArray, publicKey: ByteArray): Boolean { - return encryptionService.verifyEd25519Signature(signature, data, publicKey) - } - - // Noise protocol operations - override fun hasNoiseSession(peerID: String): Boolean { - return encryptionService.hasEstablishedSession(peerID) - } - - override fun initiateNoiseHandshake(peerID: String) { - try { - // Initiate proper Noise handshake with specific peer - val handshakeData = encryptionService.initiateHandshake(peerID) - if (handshakeData != null) { - val packet = BitchatPacket( - version = 1u, - type = MessageType.NOISE_HANDSHAKE.value, - senderID = hexStringToByteArray(myPeerID), - recipientID = hexStringToByteArray(peerID), - timestamp = System.currentTimeMillis().toULong(), - payload = handshakeData, - ttl = MAX_TTL - ) - - // Sign the handshake packet before broadcasting - val signedPacket = signPacketBeforeBroadcast(packet) - dispatchGlobal(RoutedPacket(signedPacket)) - Log.d(TAG, "Initiated Noise handshake with $peerID (${handshakeData.size} bytes)") - } else { - Log.w(TAG, "Failed to generate Noise handshake data for $peerID") - } - - } catch (e: Exception) { - Log.e(TAG, "Failed to initiate Noise handshake with $peerID: ${e.message}") - } - } - - override fun processNoiseHandshakeMessage(payload: ByteArray, peerID: String): ByteArray? { - return try { - encryptionService.processHandshakeMessage(payload, peerID) - } catch (e: Exception) { - Log.e(TAG, "Failed to process handshake message from $peerID: ${e.message}") - null - } - } - - override fun updatePeerIDBinding(newPeerID: String, nickname: String, - publicKey: ByteArray, previousPeerID: String?) { - - Log.d(TAG, "Updating peer ID binding: $newPeerID (was: $previousPeerID) with nickname: $nickname and public key: ${publicKey.toHexString().take(16)}...") - // Update peer mapping in the PeerManager for peer ID rotation support - peerManager.addOrUpdatePeer(newPeerID, nickname) - - // Store fingerprint for the peer via centralized fingerprint manager - val fingerprint = peerManager.storeFingerprintForPeer(newPeerID, publicKey) - - // Index existing Nostr mapping by the new peerID if we have it - try { - com.bitchat.android.favorites.FavoritesPersistenceService.shared.findNostrPubkey(publicKey)?.let { npub -> - com.bitchat.android.favorites.FavoritesPersistenceService.shared.updateNostrPublicKeyForPeerID(newPeerID, npub) - } - } catch (_: Exception) { } - - // If there was a previous peer ID, remove it to avoid duplicates - previousPeerID?.let { oldPeerID -> - peerManager.removePeer(oldPeerID) - } - - Log.d(TAG, "Updated peer ID binding: $newPeerID (was: $previousPeerID), fingerprint: ${fingerprint.take(16)}...") - } - - // Message operations - override fun decryptChannelMessage(encryptedContent: ByteArray, channel: String): String? { - return delegate?.decryptChannelMessage(encryptedContent, channel) - } - - // Callbacks - override fun onMessageReceived(message: BitchatMessage) { - // Always reflect into process-wide store so UI can hydrate after recreation - try { - when { - message.isPrivate -> { - val peer = message.senderPeerID ?: "" - if (peer.isNotEmpty()) com.bitchat.android.services.AppStateStore.addPrivateMessage(peer, message) - } - message.channel != null -> { - com.bitchat.android.services.AppStateStore.addChannelMessage(message.channel!!, message) - } - else -> { - com.bitchat.android.services.AppStateStore.addPublicMessage(message) - } - } - } catch (_: Exception) { } - // And forward to UI delegate if attached - delegate?.didReceiveMessage(message) - - // If no UI delegate attached (app closed), show DM notification via service manager - if (delegate == null && message.isPrivate) { - try { - val senderPeerID = message.senderPeerID - if (senderPeerID != null) { - val nick = try { peerManager.getPeerNickname(senderPeerID) } catch (_: Exception) { null } ?: senderPeerID - val preview = com.bitchat.android.ui.NotificationTextUtils.buildPrivateMessagePreview(message) - serviceNotificationManager.setAppBackgroundState(true) - serviceNotificationManager.showPrivateMessageNotification(senderPeerID, nick, preview) - } - } catch (_: Exception) { } - } - } - - override fun onChannelLeave(channel: String, fromPeer: String) { - delegate?.didReceiveChannelLeave(channel, fromPeer) - } - - override fun onDeliveryAckReceived(messageID: String, peerID: String) { - delegate?.didReceiveDeliveryAck(messageID, peerID) - } - - override fun onReadReceiptReceived(messageID: String, peerID: String) { - delegate?.didReceiveReadReceipt(messageID, peerID) - } - } - - // PacketProcessor delegates - packetProcessor.delegate = object : PacketProcessorDelegate { - override fun validatePacketSecurity(packet: BitchatPacket, peerID: String): Boolean { - return securityManager.validatePacket(packet, peerID) - } - - override fun updatePeerLastSeen(peerID: String) { - peerManager.updatePeerLastSeen(peerID) - } - - override fun getPeerNickname(peerID: String): String? { - return peerManager.getPeerNickname(peerID) - } - - // Network information for relay manager - override fun getNetworkSize(): Int { - return peerManager.getActivePeerCount() - } - - override fun getBroadcastRecipient(): ByteArray { - return SpecialRecipients.BROADCAST - } - - override fun handleNoiseHandshake(routed: RoutedPacket): Boolean { - return runBlocking { securityManager.handleNoiseHandshake(routed) } - } - - override fun handleNoiseEncrypted(routed: RoutedPacket) { - serviceScope.launch { messageHandler.handleNoiseEncrypted(routed) } - } - - override fun handleAnnounce(routed: RoutedPacket) { - serviceScope.launch { - // Process the announce - val isFirst = messageHandler.handleAnnounce(routed) - - // Map device address -> peerID on first announce seen over this device connection - val deviceAddress = routed.relayAddress - val pid = routed.peerID - if (deviceAddress != null && pid != null) { - // First ANNOUNCE over a device connection defines a direct neighbor. - if (!connectionManager.hasSeenFirstAnnounce(deviceAddress)) { - // Bind or rebind this device address to the announcing peer - connectionManager.addressPeerMap[deviceAddress] = pid - connectionManager.noteAnnounceReceived(deviceAddress) - Log.d(TAG, "Mapped device $deviceAddress to peer $pid on FIRST-ANNOUNCE for this connection") - - // Mark as directly connected (upgrades from routed if needed) - try { peerManager.setDirectConnection(pid, true) } catch (_: Exception) { } - - // Initial sync for this newly direct peer - try { gossipSyncManager.scheduleInitialSyncToPeer(pid, 1_000) } catch (_: Exception) { } - } - } - // Track for sync - try { gossipSyncManager.onPublicPacketSeen(routed.packet) } catch (_: Exception) { } - } - } - - override fun handleMessage(routed: RoutedPacket) { - serviceScope.launch { messageHandler.handleMessage(routed) } - // Track broadcast messages for sync - try { - val pkt = routed.packet - val isBroadcast = (pkt.recipientID == null || pkt.recipientID.contentEquals(SpecialRecipients.BROADCAST)) - if (isBroadcast && pkt.type == MessageType.MESSAGE.value) { - gossipSyncManager.onPublicPacketSeen(pkt) - } - } catch (_: Exception) { } - } - - override fun handleLeave(routed: RoutedPacket) { - serviceScope.launch { messageHandler.handleLeave(routed) } - } - - override fun handleFragment(packet: BitchatPacket): BitchatPacket? { - // Track broadcast fragments for gossip sync - try { - val isBroadcast = (packet.recipientID == null || packet.recipientID.contentEquals(SpecialRecipients.BROADCAST)) - if (isBroadcast && packet.type == MessageType.FRAGMENT.value) { - gossipSyncManager.onPublicPacketSeen(packet) - } - } catch (_: Exception) { } - return fragmentManager.handleFragment(packet) - } - - override fun sendAnnouncementToPeer(peerID: String) { - this@BluetoothMeshService.sendAnnouncementToPeer(peerID) - } - - override fun sendCachedMessages(peerID: String) { - storeForwardManager.sendCachedMessages(peerID) - } - - override fun relayPacket(routed: RoutedPacket) { - dispatchGlobal(routed) - } - - override fun handleRequestSync(routed: RoutedPacket) { - // Decode request and respond with missing packets - val fromPeer = routed.peerID ?: return - val req = RequestSyncPacket.decode(routed.packet.payload) ?: return - gossipSyncManager.handleRequestSync(fromPeer, req) - } - } - - // BluetoothConnectionManager delegates connectionManager.delegate = object : BluetoothConnectionManagerDelegate { override fun onPacketReceived(packet: BitchatPacket, peerID: String, device: android.bluetooth.BluetoothDevice?) { - // Log incoming for debug graphs (do not double-count anywhere else) try { - val nick = getPeerNicknames()[peerID] + val nick = meshCore.getPeerNicknames()[peerID] com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().logIncoming( packetType = packet.type.toString(), fromPeerID = peerID, @@ -564,21 +206,19 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic fromDeviceAddress = device?.address ) } catch (_: Exception) { } - packetProcessor.processPacket(RoutedPacket(packet, peerID, device?.address)) + meshCore.processIncoming(packet, peerID, device?.address) } - + override fun onDeviceConnected(device: android.bluetooth.BluetoothDevice) { - // Send initial announcements after services are ready serviceScope.launch { Log.d(TAG, "Device connected: ${device.address}; scheduling announce") delay(200) - sendBroadcastAnnounce() + meshCore.sendBroadcastAnnounce() } - // Verbose debug: device connected try { val addr = device.address val peer = connectionManager.addressPeerMap[addr] - val nick = peer?.let { peerManager.getPeerNickname(it) } ?: "unknown" + val nick = peer?.let { meshCore.getPeerNickname(it) } ?: "unknown" com.bitchat.android.ui.debug.DebugSettingsManager.getInstance() .logPeerConnection(peer ?: "unknown", nick, addr, isInbound = !connectionManager.isClientConnection(addr)!!) } catch (_: Exception) { } @@ -587,38 +227,62 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic override fun onDeviceDisconnected(device: android.bluetooth.BluetoothDevice) { Log.d(TAG, "Device disconnected: ${device.address}") val addr = device.address - // Remove mapping and, if that was the last direct path for the peer, clear direct flag val peer = connectionManager.addressPeerMap[addr] - // ConnectionTracker has already removed the address mapping; be defensive either way connectionManager.addressPeerMap.remove(addr) if (peer != null) { val stillMapped = connectionManager.addressPeerMap.values.any { it == peer } if (!stillMapped) { - // Peer might still be reachable indirectly; mark as not-direct - try { peerManager.setDirectConnection(peer, false) } catch (_: Exception) { } + try { meshCore.setDirectConnection(peer, false) } catch (_: Exception) { } } - // Verbose debug: device disconnected try { - val nick = peerManager.getPeerNickname(peer) ?: "unknown" + val nick = meshCore.getPeerNickname(peer) ?: "unknown" com.bitchat.android.ui.debug.DebugSettingsManager.getInstance() .logPeerDisconnection(peer, nick, addr) } catch (_: Exception) { } } } - + override fun onRSSIUpdated(deviceAddress: String, rssi: Int) { - // Find the peer ID for this device address and update RSSI in PeerManager connectionManager.addressPeerMap[deviceAddress]?.let { peerID -> - peerManager.updatePeerRSSI(peerID, rssi) + meshCore.updatePeerRSSI(peerID, rssi) } } } } + + private fun handleMessageReceived(message: BitchatMessage) { + try { + when { + message.isPrivate -> { + val peer = message.senderPeerID ?: "" + if (peer.isNotEmpty()) com.bitchat.android.services.AppStateStore.addPrivateMessage(peer, message) + } + message.channel != null -> { + com.bitchat.android.services.AppStateStore.addChannelMessage(message.channel!!, message) + } + else -> { + com.bitchat.android.services.AppStateStore.addPublicMessage(message) + } + } + } catch (_: Exception) { } + + if (delegate == null && message.isPrivate) { + try { + val senderPeerID = message.senderPeerID + if (senderPeerID != null) { + val nick = try { meshCore.getPeerNickname(senderPeerID) } catch (_: Exception) { null } ?: senderPeerID + val preview = com.bitchat.android.ui.NotificationTextUtils.buildPrivateMessagePreview(message) + serviceNotificationManager.setAppBackgroundState(true) + serviceNotificationManager.showPrivateMessageNotification(senderPeerID, nick, preview) + } + } catch (_: Exception) { } + } + } /** * Start the mesh service */ - fun startServices() { + override fun startServices() { // Prevent double starts (defensive programming) if (isActive) { Log.w(TAG, "Mesh service already active, ignoring duplicate start request") @@ -634,13 +298,8 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic if (connectionManager.startServices()) { isActive = true - - // Start periodic announcements for peer discovery and connectivity - sendPeriodicBroadcastAnnounce() - Log.d(TAG, "Started periodic broadcast announcements (every 30 seconds)") - // Start periodic syncs - gossipSyncManager.start() - Log.d(TAG, "GossipSyncManager started") + meshCore.startCore() + Log.d(TAG, "MeshCore started") } else { Log.e(TAG, "Failed to start Bluetooth services") } @@ -649,7 +308,7 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic /** * Stop all mesh services */ - fun stopServices() { + override fun stopServices() { if (!isActive) { Log.w(TAG, "Mesh service not active, ignoring stop request") return @@ -662,23 +321,18 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic TransportBridgeService.unregister("BLE") // Send leave announcement - sendLeaveAnnouncement() + meshCore.sendLeaveAnnouncement() serviceScope.launch { Log.d(TAG, "Stopping subcomponents and cancelling scope...") delay(200) // Give leave message time to send // Stop all components - gossipSyncManager.stop() - Log.d(TAG, "GossipSyncManager stopped") + meshCore.stopCore() + Log.d(TAG, "MeshCore stopped") connectionManager.stopServices() Log.d(TAG, "BluetoothConnectionManager stop requested") - peerManager.shutdown() - fragmentManager.shutdown() - securityManager.shutdown() - storeForwardManager.shutdown() - messageHandler.shutdown() - packetProcessor.shutdown() + meshCore.shutdown() // Mark this instance as terminated and cancel its scope so it won't be reused terminated = true @@ -702,133 +356,26 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic /** * Send public message */ - fun sendMessage(content: String, mentions: List = emptyList(), channel: String? = null) { - if (content.isEmpty()) return - - serviceScope.launch { - val packet = BitchatPacket( - version = 1u, - type = MessageType.MESSAGE.value, - senderID = hexStringToByteArray(myPeerID), - recipientID = SpecialRecipients.BROADCAST, - timestamp = System.currentTimeMillis().toULong(), - payload = content.toByteArray(Charsets.UTF_8), - signature = null, - ttl = MAX_TTL - ) - - // Sign the packet before broadcasting - val signedPacket = signPacketBeforeBroadcast(packet) - dispatchGlobal(RoutedPacket(signedPacket)) - // Track our own broadcast message for sync - try { gossipSyncManager.onPublicPacketSeen(signedPacket) } catch (_: Exception) { } - } + override fun sendMessage(content: String, mentions: List, channel: String?) { + meshCore.sendMessage(content, mentions, channel) } /** * Send a file over mesh as a broadcast MESSAGE (public mesh timeline/channels). */ - fun sendFileBroadcast(file: com.bitchat.android.model.BitchatFilePacket) { - try { - Log.d(TAG, "πŸ“€ sendFileBroadcast: name=${file.fileName}, size=${file.fileSize}") - val payload = file.encode() - if (payload == null) { - Log.e(TAG, "❌ Failed to encode file packet in sendFileBroadcast") - return - } - Log.d(TAG, "πŸ“¦ Encoded payload: ${payload.size} bytes") - serviceScope.launch { - val packet = BitchatPacket( - version = 2u, // FILE_TRANSFER uses v2 for 4-byte payload length to support large files - type = MessageType.FILE_TRANSFER.value, - senderID = hexStringToByteArray(myPeerID), - recipientID = SpecialRecipients.BROADCAST, - timestamp = System.currentTimeMillis().toULong(), - payload = payload, - signature = null, - ttl = MAX_TTL - ) - val signed = signPacketBeforeBroadcast(packet) - // Use a stable transferId based on the file TLV payload for progress tracking - val transferId = sha256Hex(payload) - dispatchGlobal(RoutedPacket(signed, transferId = transferId)) - try { gossipSyncManager.onPublicPacketSeen(signed) } catch (_: Exception) { } - } - } catch (e: Exception) { - Log.e(TAG, "❌ sendFileBroadcast failed: ${e.message}", e) - Log.e(TAG, "❌ File: name=${file.fileName}, size=${file.fileSize}") - } + override fun sendFileBroadcast(file: com.bitchat.android.model.BitchatFilePacket) { + meshCore.sendFileBroadcast(file) } /** * Send a file as an encrypted private message using Noise protocol */ - fun sendFilePrivate(recipientPeerID: String, file: com.bitchat.android.model.BitchatFilePacket) { - try { - Log.d(TAG, "πŸ“€ sendFilePrivate (ENCRYPTED): to=$recipientPeerID, name=${file.fileName}, size=${file.fileSize}") - - serviceScope.launch { - // Check if we have an established Noise session - if (encryptionService.hasEstablishedSession(recipientPeerID)) { - try { - // Encode the file packet as TLV - val filePayload = file.encode() - if (filePayload == null) { - Log.e(TAG, "❌ Failed to encode file packet for private send") - return@launch - } - Log.d(TAG, "πŸ“¦ Encoded file TLV: ${filePayload.size} bytes") - - // Create NoisePayload wrapper (type byte + file TLV data) - same as iOS - val noisePayload = com.bitchat.android.model.NoisePayload( - type = com.bitchat.android.model.NoisePayloadType.FILE_TRANSFER, - data = filePayload - ) - - // Encrypt the payload using Noise - val encrypted = encryptionService.encrypt(noisePayload.encode(), recipientPeerID) - if (encrypted == null) { - Log.e(TAG, "❌ Failed to encrypt file for $recipientPeerID") - return@launch - } - Log.d(TAG, "πŸ” Encrypted file payload: ${encrypted.size} bytes") - - // Create NOISE_ENCRYPTED packet (not FILE_TRANSFER!) - val packet = BitchatPacket( - version = 1u, - type = MessageType.NOISE_ENCRYPTED.value, - senderID = hexStringToByteArray(myPeerID), - recipientID = hexStringToByteArray(recipientPeerID), - timestamp = System.currentTimeMillis().toULong(), - payload = encrypted, - signature = null, - ttl = com.bitchat.android.util.AppConstants.MESSAGE_TTL_HOPS - ) - - // Sign and send the encrypted packet - val signed = signPacketBeforeBroadcast(packet) - // Use a stable transferId based on the unencrypted file TLV payload for progress tracking - val transferId = sha256Hex(filePayload) - dispatchGlobal(RoutedPacket(signed, transferId = transferId)) - Log.d(TAG, "βœ… Sent encrypted file to $recipientPeerID") - - } catch (e: Exception) { - Log.e(TAG, "❌ Failed to encrypt file for $recipientPeerID: ${e.message}", e) - } - } else { - // No session - initiate handshake but don't queue file - Log.w(TAG, "⚠️ No Noise session with $recipientPeerID for file transfer, initiating handshake") - messageHandler.delegate?.initiateNoiseHandshake(recipientPeerID) - } - } - } catch (e: Exception) { - Log.e(TAG, "❌ sendFilePrivate failed: ${e.message}", e) - Log.e(TAG, "❌ File: to=$recipientPeerID, name=${file.fileName}, size=${file.fileSize}") - } + override fun sendFilePrivate(recipientPeerID: String, file: com.bitchat.android.model.BitchatFilePacket) { + meshCore.sendFilePrivate(recipientPeerID, file) } - fun cancelFileTransfer(transferId: String): Boolean { - return connectionManager.cancelTransfer(transferId) + override fun cancelFileTransfer(transferId: String): Boolean { + return meshCore.cancelFileTransfer(transferId) } // Local helper to hash payloads to a stable hex ID for progress mapping @@ -842,433 +389,149 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic * Send private message - SIMPLIFIED iOS-compatible version * Uses NoisePayloadType system exactly like iOS SimplifiedBluetoothService */ - fun sendPrivateMessage(content: String, recipientPeerID: String, recipientNickname: String, messageID: String? = null) { - if (content.isEmpty() || recipientPeerID.isEmpty()) return - if (recipientNickname.isEmpty()) return - - serviceScope.launch { - val finalMessageID = messageID ?: java.util.UUID.randomUUID().toString() - - Log.d(TAG, "πŸ“¨ Sending PM to $recipientPeerID: ${content.take(30)}...") - - // Check if we have an established Noise session - if (encryptionService.hasEstablishedSession(recipientPeerID)) { - try { - // Create TLV-encoded private message exactly like iOS - val privateMessage = com.bitchat.android.model.PrivateMessagePacket( - messageID = finalMessageID, - content = content - ) - - val tlvData = privateMessage.encode() - if (tlvData == null) { - Log.e(TAG, "Failed to encode private message with TLV") - return@launch - } - - // Create message payload with NoisePayloadType prefix: [type byte] + [TLV data] - val messagePayload = com.bitchat.android.model.NoisePayload( - type = com.bitchat.android.model.NoisePayloadType.PRIVATE_MESSAGE, - data = tlvData - ) - - // Encrypt the payload - val encrypted = encryptionService.encrypt(messagePayload.encode(), recipientPeerID) - - // Create NOISE_ENCRYPTED packet exactly like iOS - val packet = BitchatPacket( - version = 1u, - type = MessageType.NOISE_ENCRYPTED.value, - senderID = hexStringToByteArray(myPeerID), - recipientID = hexStringToByteArray(recipientPeerID), - timestamp = System.currentTimeMillis().toULong(), - payload = encrypted, - signature = null, - ttl = MAX_TTL - ) - - // Sign the packet before broadcasting - val signedPacket = signPacketBeforeBroadcast(packet) - dispatchGlobal(RoutedPacket(signedPacket)) - Log.d(TAG, "πŸ“€ Sent encrypted private message to $recipientPeerID (${encrypted.size} bytes)") - - // FIXED: Don't send didReceiveMessage for our own sent messages - // This was causing self-notifications - iOS doesn't do this - // The UI handles showing sent messages through its own message sending logic - - } catch (e: Exception) { - Log.e(TAG, "Failed to encrypt private message for $recipientPeerID: ${e.message}") - } - } else { - // Fire and forget - initiate handshake but don't queue exactly like iOS - Log.d(TAG, "🀝 No session with $recipientPeerID, initiating handshake") - messageHandler.delegate?.initiateNoiseHandshake(recipientPeerID) - - // FIXED: Don't send didReceiveMessage for our own sent messages - // The UI will handle showing the message in the chat interface - } - } + override fun sendPrivateMessage(content: String, recipientPeerID: String, recipientNickname: String, messageID: String?) { + meshCore.sendPrivateMessage(content, recipientPeerID, recipientNickname, messageID) } /** * Send read receipt for a received private message - NEW NoisePayloadType implementation * Uses same encryption approach as iOS SimplifiedBluetoothService */ - fun sendReadReceipt(messageID: String, recipientPeerID: String, readerNickname: String) { - serviceScope.launch { - Log.d(TAG, "πŸ“– Sending read receipt for message $messageID to $recipientPeerID") - - // Route geohash read receipts via MessageRouter instead of here - val geo = runCatching { com.bitchat.android.services.MessageRouter.tryGetInstance() }.getOrNull() - val isGeoAlias = try { - val map = com.bitchat.android.nostr.GeohashAliasRegistry.snapshot() - map.containsKey(recipientPeerID) - } catch (_: Exception) { false } - if (isGeoAlias && geo != null) { - geo.sendReadReceipt(com.bitchat.android.model.ReadReceipt(messageID), recipientPeerID) - return@launch - } - - try { - // Avoid duplicate read receipts: check persistent store first - val seenStore = try { com.bitchat.android.services.SeenMessageStore.getInstance(context.applicationContext) } catch (_: Exception) { null } - if (seenStore?.hasRead(messageID) == true) { - Log.d(TAG, "Skipping read receipt for $messageID - already marked read") - return@launch - } - - // Create read receipt payload using NoisePayloadType exactly like iOS - val readReceiptPayload = com.bitchat.android.model.NoisePayload( - type = com.bitchat.android.model.NoisePayloadType.READ_RECEIPT, - data = messageID.toByteArray(Charsets.UTF_8) - ) - - // Encrypt the payload - val encrypted = encryptionService.encrypt(readReceiptPayload.encode(), recipientPeerID) - - // Create NOISE_ENCRYPTED packet exactly like iOS - val packet = BitchatPacket( - version = 1u, - type = MessageType.NOISE_ENCRYPTED.value, - senderID = hexStringToByteArray(myPeerID), - recipientID = hexStringToByteArray(recipientPeerID), - timestamp = System.currentTimeMillis().toULong(), - payload = encrypted, - signature = null, - ttl = com.bitchat.android.util.AppConstants.MESSAGE_TTL_HOPS // Same TTL as iOS messageTTL - ) - - // Sign the packet before broadcasting - val signedPacket = signPacketBeforeBroadcast(packet) - dispatchGlobal(RoutedPacket(signedPacket)) - Log.d(TAG, "πŸ“€ Sent read receipt to $recipientPeerID for message $messageID") - - // Persist as read after successful send - try { seenStore?.markRead(messageID) } catch (_: Exception) { } - - } catch (e: Exception) { - Log.e(TAG, "Failed to send read receipt to $recipientPeerID: ${e.message}") - } - } + override fun sendReadReceipt(messageID: String, recipientPeerID: String, readerNickname: String) { + meshCore.sendReadReceipt(messageID, recipientPeerID, readerNickname) } /** * Send broadcast announce with TLV-encoded identity announcement - exactly like iOS */ - fun sendBroadcastAnnounce() { - Log.d(TAG, "Sending broadcast announce") - serviceScope.launch { - val nickname = try { com.bitchat.android.services.NicknameProvider.getNickname(context, myPeerID) } catch (_: Exception) { myPeerID } - - // Get the static public key for the announcement - val staticKey = encryptionService.getStaticPublicKey() - if (staticKey == null) { - Log.e(TAG, "No static public key available for announcement") - return@launch - } - - // Get the signing public key for the announcement - val signingKey = encryptionService.getSigningPublicKey() - if (signingKey == null) { - Log.e(TAG, "No signing public key available for announcement") - return@launch - } - - // Create iOS-compatible IdentityAnnouncement with TLV encoding - val announcement = IdentityAnnouncement(nickname, staticKey, signingKey) - val tlvPayload = announcement.encode() - if (tlvPayload == null) { - Log.e(TAG, "Failed to encode announcement as TLV") - return@launch - } - - val announcePacket = BitchatPacket( - type = MessageType.ANNOUNCE.value, - ttl = MAX_TTL, - senderID = myPeerID, - payload = tlvPayload - ) - - // Sign the packet using our signing key (exactly like iOS) - val signedPacket = encryptionService.signData(announcePacket.toBinaryDataForSigning()!!)?.let { signature -> - announcePacket.copy(signature = signature) - } ?: announcePacket - - dispatchGlobal(RoutedPacket(signedPacket)) - Log.d(TAG, "Sent iOS-compatible signed TLV announce (${tlvPayload.size} bytes)") - // Track announce for sync - try { gossipSyncManager.onPublicPacketSeen(signedPacket) } catch (_: Exception) { } - } + override fun sendBroadcastAnnounce() { + meshCore.sendBroadcastAnnounce() } /** * Send announcement to specific peer with TLV-encoded identity announcement - exactly like iOS */ - fun sendAnnouncementToPeer(peerID: String) { - if (peerManager.hasAnnouncedToPeer(peerID)) return - - val nickname = try { com.bitchat.android.services.NicknameProvider.getNickname(context, myPeerID) } catch (_: Exception) { myPeerID } - - // Get the static public key for the announcement - val staticKey = encryptionService.getStaticPublicKey() - if (staticKey == null) { - Log.e(TAG, "No static public key available for peer announcement") - return - } - - // Get the signing public key for the announcement - val signingKey = encryptionService.getSigningPublicKey() - if (signingKey == null) { - Log.e(TAG, "No signing public key available for peer announcement") - return - } - - // Create iOS-compatible IdentityAnnouncement with TLV encoding - val announcement = IdentityAnnouncement(nickname, staticKey, signingKey) - val tlvPayload = announcement.encode() - if (tlvPayload == null) { - Log.e(TAG, "Failed to encode peer announcement as TLV") - return - } - - val packet = BitchatPacket( - type = MessageType.ANNOUNCE.value, - ttl = MAX_TTL, - senderID = myPeerID, - payload = tlvPayload - ) - - // Sign the packet using our signing key (exactly like iOS) - val signedPacket = encryptionService.signData(packet.toBinaryDataForSigning()!!)?.let { signature -> - packet.copy(signature = signature) - } ?: packet - - dispatchGlobal(RoutedPacket(signedPacket)) - peerManager.markPeerAsAnnouncedTo(peerID) - Log.d(TAG, "Sent iOS-compatible signed TLV peer announce to $peerID (${tlvPayload.size} bytes)") - - // Track announce for sync - try { gossipSyncManager.onPublicPacketSeen(signedPacket) } catch (_: Exception) { } + override fun sendAnnouncementToPeer(peerID: String) { + meshCore.sendAnnouncementToPeer(peerID) } - /** - * Send leave announcement - */ - private fun sendLeaveAnnouncement() { - val packet = BitchatPacket( - type = MessageType.LEAVE.value, - ttl = MAX_TTL, - senderID = myPeerID, - payload = byteArrayOf() - ) - - // Sign the packet before broadcasting - val signedPacket = signPacketBeforeBroadcast(packet) - dispatchGlobal(RoutedPacket(signedPacket)) - } - /** * Get peer nicknames */ - fun getPeerNicknames(): Map = peerManager.getAllPeerNicknames() + override fun getPeerNicknames(): Map = meshCore.getPeerNicknames() /** * Get peer RSSI values */ - fun getPeerRSSI(): Map = peerManager.getAllPeerRSSI() + override fun getPeerRSSI(): Map = meshCore.getPeerRSSI() /** * Check if we have an established Noise session with a peer */ - fun hasEstablishedSession(peerID: String): Boolean { - return encryptionService.hasEstablishedSession(peerID) + override fun hasEstablishedSession(peerID: String): Boolean { + return meshCore.hasEstablishedSession(peerID) } /** * Get session state for a peer (for UI state display) */ - fun getSessionState(peerID: String): com.bitchat.android.noise.NoiseSession.NoiseSessionState { - return encryptionService.getSessionState(peerID) + override fun getSessionState(peerID: String): com.bitchat.android.noise.NoiseSession.NoiseSessionState { + return meshCore.getSessionState(peerID) } /** * Initiate Noise handshake with a specific peer (public API) */ - fun initiateNoiseHandshake(peerID: String) { - // Delegate to the existing implementation in the MessageHandler delegate - messageHandler.delegate?.initiateNoiseHandshake(peerID) + override fun initiateNoiseHandshake(peerID: String) { + meshCore.initiateNoiseHandshake(peerID) } /** * Get peer fingerprint for identity management */ - fun getPeerFingerprint(peerID: String): String? { - return peerManager.getFingerprintForPeer(peerID) + override fun getPeerFingerprint(peerID: String): String? { + return meshCore.getPeerFingerprint(peerID) } /** * Get current active peer count (for status/notifications) */ - fun getActivePeerCount(): Int { - return try { peerManager.getActivePeerCount() } catch (_: Exception) { 0 } + override fun getActivePeerCount(): Int { + return meshCore.getActivePeerCount() } /** * Get peer info for verification purposes */ - fun getPeerInfo(peerID: String): PeerInfo? { - return peerManager.getPeerInfo(peerID) + override fun getPeerInfo(peerID: String): PeerInfo? { + return meshCore.getPeerInfo(peerID) } /** * Update peer information with verification data */ - fun updatePeerInfo( + override fun updatePeerInfo( peerID: String, nickname: String, noisePublicKey: ByteArray, signingPublicKey: ByteArray, isVerified: Boolean ): Boolean { - return peerManager.updatePeerInfo(peerID, nickname, noisePublicKey, signingPublicKey, isVerified) + return meshCore.updatePeerInfo(peerID, nickname, noisePublicKey, signingPublicKey, isVerified) } /** * Get our identity fingerprint */ - fun getIdentityFingerprint(): String { - return encryptionService.getIdentityFingerprint() + override fun getIdentityFingerprint(): String { + return meshCore.getIdentityFingerprint() } /** * Check if encryption icon should be shown for a peer */ - fun shouldShowEncryptionIcon(peerID: String): Boolean { - return encryptionService.hasEstablishedSession(peerID) + override fun shouldShowEncryptionIcon(peerID: String): Boolean { + return meshCore.shouldShowEncryptionIcon(peerID) } /** * Get all peers with established encrypted sessions */ - fun getEncryptedPeers(): List { - // SIMPLIFIED: Return empty list for now since we don't have direct access to sessionManager - // This method is not critical for the session retention fix - return emptyList() + override fun getEncryptedPeers(): List { + return meshCore.getEncryptedPeers() } /** * Get device address for a specific peer ID */ - fun getDeviceAddressForPeer(peerID: String): String? { - return connectionManager.addressPeerMap.entries.find { it.value == peerID }?.key + override fun getDeviceAddressForPeer(peerID: String): String? { + return meshCore.getDeviceAddressForPeer(peerID) } /** * Get all device addresses mapped to their peer IDs */ - fun getDeviceAddressToPeerMapping(): Map { - return connectionManager.addressPeerMap.toMap() + override fun getDeviceAddressToPeerMapping(): Map { + return meshCore.getDeviceAddressToPeerMapping() } /** * Print device addresses for all connected peers */ - fun printDeviceAddressesForPeers(): String { - return peerManager.getDebugInfoWithDeviceAddresses(connectionManager.addressPeerMap) + override fun printDeviceAddressesForPeers(): String { + return meshCore.getDebugInfoWithDeviceAddresses(connectionManager.addressPeerMap) } /** * Get debug status information */ - fun getDebugStatus(): String { - return buildString { - appendLine("=== Bluetooth Mesh Service Debug Status ===") - appendLine("My Peer ID: $myPeerID") - appendLine() - appendLine(connectionManager.getDebugInfo()) - appendLine() - appendLine(peerManager.getDebugInfo(connectionManager.addressPeerMap)) - appendLine() - appendLine(peerManager.getFingerprintDebugInfo()) - appendLine() - appendLine(fragmentManager.getDebugInfo()) - appendLine() - appendLine(securityManager.getDebugInfo()) - appendLine() - appendLine(storeForwardManager.getDebugInfo()) - appendLine() - appendLine(messageHandler.getDebugInfo()) - appendLine() - appendLine(packetProcessor.getDebugInfo()) - } - } - - /** - * Convert hex string peer ID to binary data (8 bytes) - exactly same as iOS - */ - private fun hexStringToByteArray(hexString: String): ByteArray { - val result = ByteArray(8) { 0 } // Initialize with zeros, exactly 8 bytes - var tempID = hexString - var index = 0 - - while (tempID.length >= 2 && index < 8) { - val hexByte = tempID.substring(0, 2) - val byte = hexByte.toIntOrNull(16)?.toByte() - if (byte != null) { - result[index] = byte - } - tempID = tempID.substring(2) - index++ - } - - return result - } - - /** - * Sign packet before broadcasting using our signing private key - */ - private fun signPacketBeforeBroadcast(packet: BitchatPacket): BitchatPacket { - return try { - // Get the canonical packet data for signing (without signature) - val packetDataForSigning = packet.toBinaryDataForSigning() - if (packetDataForSigning == null) { - Log.w(TAG, "Failed to encode packet type ${packet.type} for signing, sending unsigned") - return packet - } - - // Sign the packet data using our signing key - val signature = encryptionService.signData(packetDataForSigning) - if (signature != null) { - Log.d(TAG, "βœ… Signed packet type ${packet.type} (signature ${signature.size} bytes)") - packet.copy(signature = signature) - } else { - Log.w(TAG, "Failed to sign packet type ${packet.type}, sending unsigned") - packet - } - } catch (e: Exception) { - Log.w(TAG, "Error signing packet type ${packet.type}: ${e.message}, sending unsigned") - packet - } + override fun getDebugStatus(): String { + return meshCore.getDebugStatus( + transportInfo = connectionManager.getDebugInfo(), + deviceMap = connectionManager.addressPeerMap, + extraLines = listOf(meshCore.getFingerprintDebugInfo()), + title = "Bluetooth Mesh Service Debug Status" + ) } // MARK: - Panic Mode Support @@ -1276,15 +539,10 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic /** * Clear all internal mesh service data (for panic mode) */ - fun clearAllInternalData() { + override fun clearAllInternalData() { Log.w(TAG, "🚨 Clearing all mesh service internal data") try { - // Clear all managers - fragmentManager.clearAllFragments() - storeForwardManager.clearAllCache() - securityManager.clearAllData() - peerManager.clearAllPeers() - peerManager.clearAllFingerprints() + meshCore.clearAllInternalData() Log.d(TAG, "βœ… Cleared all mesh service internal data") } catch (e: Exception) { Log.e(TAG, "❌ Error clearing mesh service internal data: ${e.message}") @@ -1294,29 +552,42 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic /** * Clear all encryption and cryptographic data (for panic mode) */ - fun clearAllEncryptionData() { + override fun clearAllEncryptionData() { Log.w(TAG, "🚨 Clearing all encryption data") try { - // Clear encryption service persistent identity (includes Ed25519 signing keys) - encryptionService.clearPersistentIdentity() + meshCore.clearAllEncryptionData() Log.d(TAG, "βœ… Cleared all encryption data") } catch (e: Exception) { Log.e(TAG, "❌ Error clearing encryption data: ${e.message}") } } -} -/** - * Delegate interface for mesh service callbacks (maintains exact same interface) - */ -interface BluetoothMeshDelegate { - fun didReceiveMessage(message: BitchatMessage) - fun didUpdatePeerList(peers: List) - fun didReceiveChannelLeave(channel: String, fromPeer: String) - fun didReceiveDeliveryAck(messageID: String, recipientPeerID: String) - fun didReceiveReadReceipt(messageID: String, recipientPeerID: String) - fun decryptChannelMessage(encryptedContent: ByteArray, channel: String): String? - fun getNickname(): String? - fun isFavorite(peerID: String): Boolean - // registerPeerPublicKey REMOVED - fingerprints now handled centrally in PeerManager + private inner class BleMeshTransport : MeshTransport { + override val id: String = "BLE" + var connectionManager: BluetoothConnectionManager? = null + + override fun broadcastPacket(routed: RoutedPacket) { + connectionManager?.broadcastPacket(routed) + } + + override fun sendPacketToPeer(peerID: String, packet: BitchatPacket) { + connectionManager?.sendPacketToPeer(peerID, packet) + } + + override fun cancelTransfer(transferId: String): Boolean { + return connectionManager?.cancelTransfer(transferId) ?: false + } + + override fun getDeviceAddressForPeer(peerID: String): String? { + return connectionManager?.addressPeerMap?.entries?.find { it.value == peerID }?.key + } + + override fun getDeviceAddressToPeerMapping(): Map { + return connectionManager?.addressPeerMap?.toMap() ?: emptyMap() + } + + override fun getTransportDebugInfo(): String { + return connectionManager?.getDebugInfo() ?: "" + } + } } diff --git a/app/src/main/java/com/bitchat/android/mesh/MeshCore.kt b/app/src/main/java/com/bitchat/android/mesh/MeshCore.kt new file mode 100644 index 00000000..ae2db674 --- /dev/null +++ b/app/src/main/java/com/bitchat/android/mesh/MeshCore.kt @@ -0,0 +1,716 @@ +package com.bitchat.android.mesh + +import android.content.Context +import android.util.Log +import com.bitchat.android.crypto.EncryptionService +import com.bitchat.android.model.BitchatMessage +import com.bitchat.android.model.BitchatFilePacket +import com.bitchat.android.model.IdentityAnnouncement +import com.bitchat.android.model.NoisePayload +import com.bitchat.android.model.NoisePayloadType +import com.bitchat.android.model.PrivateMessagePacket +import com.bitchat.android.model.RequestSyncPacket +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 kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking + +/** + * Shared mesh coordinator that wires all mesh-layer components and provides common APIs + * for send/receive operations across transports. + */ +class MeshCore( + private val context: Context, + private val scope: CoroutineScope, + private val transport: MeshTransport, + private val encryptionService: EncryptionService, + val myPeerID: String, + private val maxTtl: UByte, + sharedGossipManager: GossipSyncManager?, + gossipConfigProvider: GossipSyncManager.ConfigProvider, + private val hooks: Hooks = Hooks() +) { + data class Hooks( + val onMessageReceived: ((BitchatMessage) -> Unit)? = null, + val onPeerIdBindingUpdated: ((String, String, ByteArray, String?) -> Unit)? = null, + val onAnnounceProcessed: ((RoutedPacket, Boolean) -> Unit)? = null, + val readReceiptInterceptor: ((String, String) -> Boolean)? = null, + val onReadReceiptSent: ((String) -> Unit)? = null, + val announcementNicknameProvider: (() -> String?)? = null, + val leavePayloadProvider: (() -> ByteArray)? = null + ) + + private val peerManager = PeerManager() + val fragmentManager = FragmentManager() + private val securityManager = SecurityManager(encryptionService, myPeerID) + private val storeForwardManager = StoreForwardManager() + private val messageHandler = MessageHandler(myPeerID, context.applicationContext) + private val packetProcessor = PacketProcessor(myPeerID) + + val gossipSyncManager: GossipSyncManager = + sharedGossipManager ?: GossipSyncManager(myPeerID = myPeerID, scope = scope, configProvider = gossipConfigProvider) + + var delegate: MeshDelegate? = null + + private var announceJob: Job? = null + private var isActive = false + + init { + messageHandler.packetProcessor = packetProcessor + setupDelegates() + + if (sharedGossipManager == null) { + gossipSyncManager.delegate = object : GossipSyncManager.Delegate { + override fun sendPacket(packet: BitchatPacket) { + dispatchGlobal(RoutedPacket(packet)) + } + + override fun sendPacketToPeer(peerID: String, packet: BitchatPacket) { + transport.sendPacketToPeer(peerID, packet) + } + + override fun signPacketForBroadcast(packet: BitchatPacket): BitchatPacket { + return signPacketBeforeBroadcast(packet) + } + } + } + } + + fun startCore() { + if (isActive) return + isActive = true + startPeriodicBroadcastAnnounce() + gossipSyncManager.start() + } + + fun stopCore() { + if (!isActive) return + isActive = false + announceJob?.cancel() + announceJob = null + gossipSyncManager.stop() + } + + fun shutdown() { + peerManager.shutdown() + fragmentManager.shutdown() + securityManager.shutdown() + storeForwardManager.shutdown() + messageHandler.shutdown() + packetProcessor.shutdown() + } + + fun processIncoming(packet: BitchatPacket, peerID: String?, relayAddress: String?) { + packetProcessor.processPacket(RoutedPacket(packet, peerID, relayAddress)) + } + + fun sendFromBridge(packet: RoutedPacket) { + transport.broadcastPacket(packet) + } + + private fun dispatchGlobal(routed: RoutedPacket) { + transport.broadcastPacket(routed) + TransportBridgeService.broadcast(transport.id, routed) + } + + private fun startPeriodicBroadcastAnnounce() { + announceJob?.cancel() + announceJob = scope.launch { + while (isActive) { + try { + delay(30_000) + sendBroadcastAnnounce() + } catch (_: Exception) { } + } + } + } + + private fun setupDelegates() { + peerManager.delegate = object : PeerManagerDelegate { + override fun onPeerListUpdated(peerIDs: List) { + delegate?.didUpdatePeerList(peerIDs) + } + + override fun onPeerRemoved(peerID: String) { + try { gossipSyncManager.removeAnnouncementForPeer(peerID) } catch (_: Exception) { } + try { encryptionService.removePeer(peerID) } catch (_: Exception) { } + } + } + + securityManager.delegate = object : SecurityManagerDelegate { + override fun onKeyExchangeCompleted(peerID: String, peerPublicKeyData: ByteArray) { + scope.launch { + delay(100) + sendAnnouncementToPeer(peerID) + delay(1000) + storeForwardManager.sendCachedMessages(peerID) + } + } + + override fun sendHandshakeResponse(peerID: String, response: ByteArray) { + val responsePacket = BitchatPacket( + version = 1u, + type = MessageType.NOISE_HANDSHAKE.value, + senderID = MeshPacketUtils.hexStringToByteArray(myPeerID), + recipientID = MeshPacketUtils.hexStringToByteArray(peerID), + timestamp = System.currentTimeMillis().toULong(), + payload = response, + ttl = maxTtl + ) + dispatchGlobal(RoutedPacket(signPacketBeforeBroadcast(responsePacket))) + } + + override fun getPeerInfo(peerID: String): PeerInfo? = peerManager.getPeerInfo(peerID) + } + + storeForwardManager.delegate = object : StoreForwardManagerDelegate { + override fun isFavorite(peerID: String): Boolean { + return delegate?.isFavorite(peerID) ?: false + } + + override fun isPeerOnline(peerID: String): Boolean { + return peerManager.isPeerActive(peerID) + } + + override fun sendPacket(packet: BitchatPacket) { + dispatchGlobal(RoutedPacket(packet)) + } + } + + messageHandler.delegate = object : MessageHandlerDelegate { + override fun addOrUpdatePeer(peerID: String, nickname: String): Boolean { + return peerManager.addOrUpdatePeer(peerID, nickname) + } + + override fun removePeer(peerID: String) { + peerManager.removePeer(peerID) + } + + override fun updatePeerNickname(peerID: String, nickname: String) { + peerManager.addOrUpdatePeer(peerID, nickname) + } + + override fun getPeerNickname(peerID: String): String? { + return peerManager.getPeerNickname(peerID) + } + + override fun getNetworkSize(): Int { + return peerManager.getActivePeerCount() + } + + override fun getMyNickname(): String? { + return delegate?.getNickname() + } + + override fun getPeerInfo(peerID: String): PeerInfo? { + return peerManager.getPeerInfo(peerID) + } + + override fun updatePeerInfo( + peerID: String, + nickname: String, + noisePublicKey: ByteArray, + signingPublicKey: ByteArray, + isVerified: Boolean + ): Boolean { + return peerManager.updatePeerInfo(peerID, nickname, noisePublicKey, signingPublicKey, isVerified) + } + + override fun sendPacket(packet: BitchatPacket) { + val signedPacket = signPacketBeforeBroadcast(packet) + dispatchGlobal(RoutedPacket(signedPacket)) + } + + override fun relayPacket(routed: RoutedPacket) { + dispatchGlobal(routed) + } + + override fun getBroadcastRecipient(): ByteArray { + return SpecialRecipients.BROADCAST + } + + override fun verifySignature(packet: BitchatPacket, peerID: String): Boolean { + return securityManager.verifySignature(packet, peerID) + } + + override fun encryptForPeer(data: ByteArray, recipientPeerID: String): ByteArray? { + return securityManager.encryptForPeer(data, recipientPeerID) + } + + override fun decryptFromPeer(encryptedData: ByteArray, senderPeerID: String): ByteArray? { + return securityManager.decryptFromPeer(encryptedData, senderPeerID) + } + + override fun verifyEd25519Signature(signature: ByteArray, data: ByteArray, publicKey: ByteArray): Boolean { + return encryptionService.verifyEd25519Signature(signature, data, publicKey) + } + + override fun hasNoiseSession(peerID: String): Boolean { + return encryptionService.hasEstablishedSession(peerID) + } + + override fun initiateNoiseHandshake(peerID: String) { + this@MeshCore.initiateNoiseHandshake(peerID) + } + + override fun processNoiseHandshakeMessage(payload: ByteArray, peerID: String): ByteArray? { + return try { + encryptionService.processHandshakeMessage(payload, peerID) + } catch (_: Exception) { + null + } + } + + override fun updatePeerIDBinding( + newPeerID: String, + nickname: String, + publicKey: ByteArray, + previousPeerID: String? + ) { + peerManager.addOrUpdatePeer(newPeerID, nickname) + val fingerprint = peerManager.storeFingerprintForPeer(newPeerID, publicKey) + previousPeerID?.let { peerManager.removePeer(it) } + Log.d("MeshCore", "Updated peer ID binding: $newPeerID fp=${fingerprint.take(16)}") + hooks.onPeerIdBindingUpdated?.invoke(newPeerID, nickname, publicKey, previousPeerID) + } + + override fun decryptChannelMessage(encryptedContent: ByteArray, channel: String): String? { + return delegate?.decryptChannelMessage(encryptedContent, channel) + } + + override fun onMessageReceived(message: BitchatMessage) { + hooks.onMessageReceived?.invoke(message) + delegate?.didReceiveMessage(message) + } + + override fun onChannelLeave(channel: String, fromPeer: String) { + delegate?.didReceiveChannelLeave(channel, fromPeer) + } + + override fun onDeliveryAckReceived(messageID: String, peerID: String) { + delegate?.didReceiveDeliveryAck(messageID, peerID) + } + + override fun onReadReceiptReceived(messageID: String, peerID: String) { + delegate?.didReceiveReadReceipt(messageID, peerID) + } + } + + packetProcessor.delegate = object : PacketProcessorDelegate { + override fun validatePacketSecurity(packet: BitchatPacket, peerID: String): Boolean { + return securityManager.validatePacket(packet, peerID) + } + + override fun updatePeerLastSeen(peerID: String) { + peerManager.updatePeerLastSeen(peerID) + } + + override fun getPeerNickname(peerID: String): String? { + return peerManager.getPeerNickname(peerID) + } + + override fun getNetworkSize(): Int { + return peerManager.getActivePeerCount() + } + + override fun getBroadcastRecipient(): ByteArray { + return SpecialRecipients.BROADCAST + } + + override fun handleNoiseHandshake(routed: RoutedPacket): Boolean { + return runBlocking { securityManager.handleNoiseHandshake(routed) } + } + + override fun handleNoiseEncrypted(routed: RoutedPacket) { + scope.launch { messageHandler.handleNoiseEncrypted(routed) } + } + + override fun handleAnnounce(routed: RoutedPacket) { + scope.launch { + val isFirst = messageHandler.handleAnnounce(routed) + hooks.onAnnounceProcessed?.invoke(routed, isFirst) + try { gossipSyncManager.onPublicPacketSeen(routed.packet) } catch (_: Exception) { } + } + } + + override fun handleMessage(routed: RoutedPacket) { + scope.launch { messageHandler.handleMessage(routed) } + try { + val pkt = routed.packet + val isBroadcast = (pkt.recipientID == null || pkt.recipientID.contentEquals(SpecialRecipients.BROADCAST)) + if (isBroadcast && pkt.type == MessageType.MESSAGE.value) { + gossipSyncManager.onPublicPacketSeen(pkt) + } + } catch (_: Exception) { } + } + + override fun handleLeave(routed: RoutedPacket) { + scope.launch { messageHandler.handleLeave(routed) } + } + + override fun handleFragment(packet: BitchatPacket): BitchatPacket? { + try { + val isBroadcast = (packet.recipientID == null || packet.recipientID.contentEquals(SpecialRecipients.BROADCAST)) + if (isBroadcast && packet.type == MessageType.FRAGMENT.value) { + gossipSyncManager.onPublicPacketSeen(packet) + } + } catch (_: Exception) { } + return fragmentManager.handleFragment(packet) + } + + override fun sendAnnouncementToPeer(peerID: String) { + this@MeshCore.sendAnnouncementToPeer(peerID) + } + + override fun sendCachedMessages(peerID: String) { + storeForwardManager.sendCachedMessages(peerID) + } + + override fun relayPacket(routed: RoutedPacket) { + dispatchGlobal(routed) + } + + override fun handleRequestSync(routed: RoutedPacket) { + val fromPeer = routed.peerID ?: return + val req = RequestSyncPacket.decode(routed.packet.payload) ?: return + gossipSyncManager.handleRequestSync(fromPeer, req) + } + } + } + + fun sendMessage(content: String, mentions: List = emptyList(), channel: String? = null) { + if (content.isEmpty()) return + scope.launch { + val packet = BitchatPacket( + version = 1u, + type = MessageType.MESSAGE.value, + senderID = MeshPacketUtils.hexStringToByteArray(myPeerID), + recipientID = SpecialRecipients.BROADCAST, + timestamp = System.currentTimeMillis().toULong(), + payload = content.toByteArray(Charsets.UTF_8), + signature = null, + ttl = maxTtl + ) + val signedPacket = signPacketBeforeBroadcast(packet) + dispatchGlobal(RoutedPacket(signedPacket)) + try { gossipSyncManager.onPublicPacketSeen(signedPacket) } catch (_: Exception) { } + } + } + + fun sendFileBroadcast(file: BitchatFilePacket) { + try { + val payload = file.encode() ?: return + scope.launch { + val packet = BitchatPacket( + version = 2u, + type = MessageType.FILE_TRANSFER.value, + senderID = MeshPacketUtils.hexStringToByteArray(myPeerID), + recipientID = SpecialRecipients.BROADCAST, + timestamp = System.currentTimeMillis().toULong(), + payload = payload, + signature = null, + ttl = maxTtl + ) + val signed = signPacketBeforeBroadcast(packet) + val transferId = MeshPacketUtils.sha256Hex(payload) + dispatchGlobal(RoutedPacket(signed, transferId = transferId)) + try { gossipSyncManager.onPublicPacketSeen(signed) } catch (_: Exception) { } + } + } catch (e: Exception) { + Log.e("MeshCore", "sendFileBroadcast failed: ${e.message}", e) + } + } + + fun sendFilePrivate(recipientPeerID: String, file: BitchatFilePacket) { + try { + scope.launch { + if (!encryptionService.hasEstablishedSession(recipientPeerID)) { + initiateNoiseHandshake(recipientPeerID) + return@launch + } + val tlv = file.encode() ?: return@launch + val np = NoisePayload(type = NoisePayloadType.FILE_TRANSFER, data = tlv).encode() + val enc = encryptionService.encrypt(np, recipientPeerID) + val packet = BitchatPacket( + version = 1u, + type = MessageType.NOISE_ENCRYPTED.value, + senderID = MeshPacketUtils.hexStringToByteArray(myPeerID), + recipientID = MeshPacketUtils.hexStringToByteArray(recipientPeerID), + timestamp = System.currentTimeMillis().toULong(), + payload = enc, + signature = null, + ttl = maxTtl + ) + val signed = signPacketBeforeBroadcast(packet) + val transferId = MeshPacketUtils.sha256Hex(tlv) + dispatchGlobal(RoutedPacket(signed, transferId = transferId)) + } + } catch (e: Exception) { + Log.e("MeshCore", "sendFilePrivate failed: ${e.message}", e) + } + } + + fun cancelFileTransfer(transferId: String): Boolean { + return transport.cancelTransfer(transferId) + } + + fun sendPrivateMessage(content: String, recipientPeerID: String, recipientNickname: String, messageID: String? = null) { + if (content.isEmpty() || recipientPeerID.isEmpty()) return + scope.launch { + val finalMessageID = messageID ?: java.util.UUID.randomUUID().toString() + + if (encryptionService.hasEstablishedSession(recipientPeerID)) { + try { + val privateMessage = PrivateMessagePacket(messageID = finalMessageID, content = content) + val tlvData = privateMessage.encode() ?: return@launch + val messagePayload = NoisePayload( + type = NoisePayloadType.PRIVATE_MESSAGE, + data = tlvData + ) + val encrypted = encryptionService.encrypt(messagePayload.encode(), recipientPeerID) + val packet = BitchatPacket( + version = 1u, + type = MessageType.NOISE_ENCRYPTED.value, + senderID = MeshPacketUtils.hexStringToByteArray(myPeerID), + recipientID = MeshPacketUtils.hexStringToByteArray(recipientPeerID), + timestamp = System.currentTimeMillis().toULong(), + payload = encrypted, + signature = null, + ttl = maxTtl + ) + val signedPacket = signPacketBeforeBroadcast(packet) + dispatchGlobal(RoutedPacket(signedPacket)) + } catch (e: Exception) { + Log.e("MeshCore", "Failed to encrypt private message: ${e.message}") + } + } else { + initiateNoiseHandshake(recipientPeerID) + } + } + } + + fun sendReadReceipt(messageID: String, recipientPeerID: String, readerNickname: String) { + scope.launch { + if (hooks.readReceiptInterceptor?.invoke(messageID, recipientPeerID) == true) return@launch + try { + val payload = NoisePayload( + type = NoisePayloadType.READ_RECEIPT, + data = messageID.toByteArray(Charsets.UTF_8) + ).encode() + val enc = encryptionService.encrypt(payload, recipientPeerID) + val packet = BitchatPacket( + version = 1u, + type = MessageType.NOISE_ENCRYPTED.value, + senderID = MeshPacketUtils.hexStringToByteArray(myPeerID), + recipientID = MeshPacketUtils.hexStringToByteArray(recipientPeerID), + timestamp = System.currentTimeMillis().toULong(), + payload = enc, + signature = null, + ttl = maxTtl + ) + dispatchGlobal(RoutedPacket(signPacketBeforeBroadcast(packet))) + hooks.onReadReceiptSent?.invoke(messageID) + } catch (e: Exception) { + Log.e("MeshCore", "Failed to send read receipt: ${e.message}") + } + } + } + + fun sendBroadcastAnnounce() { + scope.launch { + val nickname = hooks.announcementNicknameProvider?.invoke() + ?: delegate?.getNickname() + ?: myPeerID + val staticKey = encryptionService.getStaticPublicKey() ?: run { + Log.e("MeshCore", "No static public key available for announcement") + return@launch + } + val signingKey = encryptionService.getSigningPublicKey() ?: run { + Log.e("MeshCore", "No signing public key available for announcement") + return@launch + } + val announcement = IdentityAnnouncement(nickname, staticKey, signingKey) + val tlvPayload = announcement.encode() ?: return@launch + val announcePacket = BitchatPacket( + type = MessageType.ANNOUNCE.value, + ttl = maxTtl, + senderID = myPeerID, + payload = tlvPayload + ) + val signedPacket = signPacketBeforeBroadcast(announcePacket) + dispatchGlobal(RoutedPacket(signedPacket)) + try { gossipSyncManager.onPublicPacketSeen(signedPacket) } catch (_: Exception) { } + } + } + + fun sendAnnouncementToPeer(peerID: String) { + if (peerManager.hasAnnouncedToPeer(peerID)) return + val nickname = hooks.announcementNicknameProvider?.invoke() + ?: delegate?.getNickname() + ?: myPeerID + val staticKey = encryptionService.getStaticPublicKey() ?: return + val signingKey = encryptionService.getSigningPublicKey() ?: return + val announcement = IdentityAnnouncement(nickname, staticKey, signingKey) + val tlvPayload = announcement.encode() ?: return + val packet = BitchatPacket( + type = MessageType.ANNOUNCE.value, + ttl = maxTtl, + senderID = myPeerID, + payload = tlvPayload + ) + val signedPacket = signPacketBeforeBroadcast(packet) + dispatchGlobal(RoutedPacket(signedPacket)) + peerManager.markPeerAsAnnouncedTo(peerID) + try { gossipSyncManager.onPublicPacketSeen(signedPacket) } catch (_: Exception) { } + } + + fun sendLeaveAnnouncement() { + val payload = hooks.leavePayloadProvider?.invoke() ?: byteArrayOf() + val packet = BitchatPacket( + type = MessageType.LEAVE.value, + ttl = maxTtl, + senderID = myPeerID, + payload = payload + ) + val signedPacket = signPacketBeforeBroadcast(packet) + dispatchGlobal(RoutedPacket(signedPacket)) + } + + fun getPeerNicknames(): Map = peerManager.getAllPeerNicknames() + + fun getPeerRSSI(): Map = peerManager.getAllPeerRSSI() + + fun getPeerNickname(peerID: String): String? = peerManager.getPeerNickname(peerID) + + fun addOrUpdatePeer(peerID: String, nickname: String): Boolean { + return peerManager.addOrUpdatePeer(peerID, nickname) + } + + fun removePeer(peerID: String) { + peerManager.removePeer(peerID) + } + + fun setDirectConnection(peerID: String, isDirect: Boolean) { + peerManager.setDirectConnection(peerID, isDirect) + } + + fun updatePeerRSSI(peerID: String, rssi: Int) { + peerManager.updatePeerRSSI(peerID, rssi) + } + + fun getDebugInfoWithDeviceAddresses(deviceMap: Map): String { + return peerManager.getDebugInfoWithDeviceAddresses(deviceMap) + } + + fun getFingerprintDebugInfo(): String { + return peerManager.getFingerprintDebugInfo() + } + + fun hasEstablishedSession(peerID: String): Boolean { + return encryptionService.hasEstablishedSession(peerID) + } + + fun getSessionState(peerID: String): com.bitchat.android.noise.NoiseSession.NoiseSessionState { + return encryptionService.getSessionState(peerID) + } + + fun initiateNoiseHandshake(peerID: String) { + scope.launch { + try { + val handshakeData = encryptionService.initiateHandshake(peerID) ?: return@launch + val packet = BitchatPacket( + version = 1u, + type = MessageType.NOISE_HANDSHAKE.value, + senderID = MeshPacketUtils.hexStringToByteArray(myPeerID), + recipientID = MeshPacketUtils.hexStringToByteArray(peerID), + timestamp = System.currentTimeMillis().toULong(), + payload = handshakeData, + ttl = maxTtl + ) + val signedPacket = signPacketBeforeBroadcast(packet) + dispatchGlobal(RoutedPacket(signedPacket)) + } catch (e: Exception) { + Log.e("MeshCore", "Failed to initiate Noise handshake with $peerID: ${e.message}") + } + } + } + + fun getPeerFingerprint(peerID: String): String? = peerManager.getFingerprintForPeer(peerID) + + fun getPeerInfo(peerID: String): PeerInfo? = peerManager.getPeerInfo(peerID) + + fun updatePeerInfo( + peerID: String, + nickname: String, + noisePublicKey: ByteArray, + signingPublicKey: ByteArray, + isVerified: Boolean + ): Boolean = peerManager.updatePeerInfo(peerID, nickname, noisePublicKey, signingPublicKey, isVerified) + + fun getIdentityFingerprint(): String = encryptionService.getIdentityFingerprint() + + fun shouldShowEncryptionIcon(peerID: String): Boolean = encryptionService.hasEstablishedSession(peerID) + + fun getEncryptedPeers(): List = emptyList() + + fun getActivePeerCount(): Int = try { peerManager.getActivePeerCount() } catch (_: Exception) { 0 } + + fun getDeviceAddressForPeer(peerID: String): String? = transport.getDeviceAddressForPeer(peerID) + + fun getDeviceAddressToPeerMapping(): Map = transport.getDeviceAddressToPeerMapping() + + fun getDebugStatus( + transportInfo: String, + deviceMap: Map, + extraLines: List = emptyList(), + title: String? = null + ): String { + return buildString { + appendLine("=== ${title ?: "${transport.id} Mesh Debug Status"} ===") + appendLine("My Peer ID: $myPeerID") + if (extraLines.isNotEmpty()) { + extraLines.forEach { appendLine(it) } + } + appendLine(transportInfo) + appendLine(peerManager.getDebugInfo(deviceMap)) + appendLine(fragmentManager.getDebugInfo()) + appendLine(securityManager.getDebugInfo()) + appendLine(storeForwardManager.getDebugInfo()) + appendLine(messageHandler.getDebugInfo()) + appendLine(packetProcessor.getDebugInfo()) + } + } + + fun clearAllInternalData() { + fragmentManager.clearAllFragments() + storeForwardManager.clearAllCache() + securityManager.clearAllData() + peerManager.clearAllPeers() + peerManager.clearAllFingerprints() + } + + fun clearAllEncryptionData() { + encryptionService.clearPersistentIdentity() + } + + private fun signPacketBeforeBroadcast(packet: BitchatPacket): BitchatPacket { + return try { + val packetDataForSigning = packet.toBinaryDataForSigning() ?: return packet + val signature = encryptionService.signData(packetDataForSigning) + if (signature != null) { + packet.copy(signature = signature) + } else { + packet + } + } catch (_: Exception) { + packet + } + } +} diff --git a/app/src/main/java/com/bitchat/android/mesh/MeshDelegate.kt b/app/src/main/java/com/bitchat/android/mesh/MeshDelegate.kt new file mode 100644 index 00000000..2c3341e5 --- /dev/null +++ b/app/src/main/java/com/bitchat/android/mesh/MeshDelegate.kt @@ -0,0 +1,19 @@ +package com.bitchat.android.mesh + +import com.bitchat.android.model.BitchatMessage + +/** + * Shared mesh delegate interface for transport-agnostic callbacks. + */ +interface MeshDelegate { + fun didReceiveMessage(message: BitchatMessage) + fun didUpdatePeerList(peers: List) + fun didReceiveChannelLeave(channel: String, fromPeer: String) + fun didReceiveDeliveryAck(messageID: String, recipientPeerID: String) + fun didReceiveReadReceipt(messageID: String, recipientPeerID: String) + fun decryptChannelMessage(encryptedContent: ByteArray, channel: String): String? + fun getNickname(): String? + fun isFavorite(peerID: String): Boolean +} + +typealias BluetoothMeshDelegate = MeshDelegate diff --git a/app/src/main/java/com/bitchat/android/mesh/MeshPacketUtils.kt b/app/src/main/java/com/bitchat/android/mesh/MeshPacketUtils.kt new file mode 100644 index 00000000..514e7a99 --- /dev/null +++ b/app/src/main/java/com/bitchat/android/mesh/MeshPacketUtils.kt @@ -0,0 +1,37 @@ +package com.bitchat.android.mesh + +/** + * Shared helpers for mesh packet handling. + */ +object MeshPacketUtils { + /** + * Convert hex string peer ID to binary data (8 bytes), matching iOS behavior. + */ + fun hexStringToByteArray(hexString: String): ByteArray { + val result = ByteArray(8) { 0 } + var tempID = hexString + var index = 0 + + while (tempID.length >= 2 && index < 8) { + val hexByte = tempID.substring(0, 2) + val byte = hexByte.toIntOrNull(16)?.toByte() + if (byte != null) { + result[index] = byte + } + tempID = tempID.substring(2) + index++ + } + return result + } + + /** + * Hash payloads to a stable hex ID for transfer tracking. + */ + fun sha256Hex(bytes: ByteArray): String = try { + val md = java.security.MessageDigest.getInstance("SHA-256") + md.update(bytes) + md.digest().joinToString("") { "%02x".format(it) } + } catch (_: Exception) { + bytes.size.toString(16) + } +} diff --git a/app/src/main/java/com/bitchat/android/mesh/MeshService.kt b/app/src/main/java/com/bitchat/android/mesh/MeshService.kt new file mode 100644 index 00000000..331b36b1 --- /dev/null +++ b/app/src/main/java/com/bitchat/android/mesh/MeshService.kt @@ -0,0 +1,51 @@ +package com.bitchat.android.mesh + +import com.bitchat.android.model.BitchatFilePacket + +/** + * Transport-agnostic mesh service API for UI and routing layers. + */ +interface MeshService { + val myPeerID: String + var delegate: MeshDelegate? + + fun startServices() + fun stopServices() + + fun sendMessage(content: String, mentions: List = emptyList(), channel: String? = null) + fun sendPrivateMessage(content: String, recipientPeerID: String, recipientNickname: String, messageID: String? = null) + fun sendReadReceipt(messageID: String, recipientPeerID: String, readerNickname: String) + fun sendFileBroadcast(file: BitchatFilePacket) + fun sendFilePrivate(recipientPeerID: String, file: BitchatFilePacket) + fun cancelFileTransfer(transferId: String): Boolean + + fun sendBroadcastAnnounce() + fun sendAnnouncementToPeer(peerID: String) + + fun getPeerNicknames(): Map + fun getPeerRSSI(): Map + fun getActivePeerCount(): Int + fun hasEstablishedSession(peerID: String): Boolean + fun getSessionState(peerID: String): com.bitchat.android.noise.NoiseSession.NoiseSessionState + fun initiateNoiseHandshake(peerID: String) + fun getPeerFingerprint(peerID: String): String? + fun getPeerInfo(peerID: String): PeerInfo? + fun updatePeerInfo( + peerID: String, + nickname: String, + noisePublicKey: ByteArray, + signingPublicKey: ByteArray, + isVerified: Boolean + ): Boolean + fun getIdentityFingerprint(): String + fun shouldShowEncryptionIcon(peerID: String): Boolean + fun getEncryptedPeers(): List + + fun getDeviceAddressForPeer(peerID: String): String? + fun getDeviceAddressToPeerMapping(): Map + fun printDeviceAddressesForPeers(): String + fun getDebugStatus(): String + + fun clearAllInternalData() + fun clearAllEncryptionData() +} diff --git a/app/src/main/java/com/bitchat/android/mesh/MeshTransport.kt b/app/src/main/java/com/bitchat/android/mesh/MeshTransport.kt new file mode 100644 index 00000000..259d696f --- /dev/null +++ b/app/src/main/java/com/bitchat/android/mesh/MeshTransport.kt @@ -0,0 +1,23 @@ +package com.bitchat.android.mesh + +import com.bitchat.android.model.RoutedPacket +import com.bitchat.android.protocol.BitchatPacket + +/** + * Transport abstraction used by MeshCore to send packets via a specific medium. + */ +interface MeshTransport { + val id: String + + fun broadcastPacket(routed: RoutedPacket) + + fun sendPacketToPeer(peerID: String, packet: BitchatPacket) + + fun cancelTransfer(transferId: String): Boolean = false + + fun getDeviceAddressForPeer(peerID: String): String? = null + + fun getDeviceAddressToPeerMapping(): Map = emptyMap() + + fun getTransportDebugInfo(): String = "" +} diff --git a/app/src/main/java/com/bitchat/android/services/MessageRouter.kt b/app/src/main/java/com/bitchat/android/services/MessageRouter.kt index 53db2fa6..98958fd4 100644 --- a/app/src/main/java/com/bitchat/android/services/MessageRouter.kt +++ b/app/src/main/java/com/bitchat/android/services/MessageRouter.kt @@ -130,7 +130,7 @@ class MessageRouter private constructor( val myNpub = try { com.bitchat.android.nostr.NostrIdentityBridge.getCurrentNostrIdentity(context)?.npub } catch (_: Exception) { null } val content = if (isFavorite) "[FAVORITED]:${myNpub ?: ""}" else "[UNFAVORITED]:${myNpub ?: ""}" val nickname = mesh.getPeerNicknames()[toPeerID] ?: toPeerID - mesh.sendPrivateMessage(content, toPeerID, nickname) + mesh.sendPrivateMessage(content, toPeerID, nickname, null) } else { nostr.sendFavoriteNotification(toPeerID, isFavorite) } diff --git a/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt b/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt index ee31ef74..86c5f67b 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt @@ -324,7 +324,7 @@ class ChatViewModel( fun leaveChannel(channel: String) { channelManager.leaveChannel(channel) - meshService.sendMessage("left $channel") + meshService.sendMessage("left $channel", emptyList(), null) } // MARK: - Private Chat Management (delegated) diff --git a/app/src/main/java/com/bitchat/android/wifi-aware/WifiAwareMeshDelegate.kt b/app/src/main/java/com/bitchat/android/wifi-aware/WifiAwareMeshDelegate.kt new file mode 100644 index 00000000..066a8b82 --- /dev/null +++ b/app/src/main/java/com/bitchat/android/wifi-aware/WifiAwareMeshDelegate.kt @@ -0,0 +1,3 @@ +package com.bitchat.android.wifiaware + +typealias WifiAwareMeshDelegate = com.bitchat.android.mesh.MeshDelegate diff --git a/app/src/main/java/com/bitchat/android/wifi-aware/WifiAwareMeshService.kt b/app/src/main/java/com/bitchat/android/wifi-aware/WifiAwareMeshService.kt index f357d715..372bba5c 100644 --- a/app/src/main/java/com/bitchat/android/wifi-aware/WifiAwareMeshService.kt +++ b/app/src/main/java/com/bitchat/android/wifi-aware/WifiAwareMeshService.kt @@ -12,32 +12,29 @@ import android.util.Log import androidx.annotation.RequiresApi import androidx.annotation.RequiresPermission import com.bitchat.android.crypto.EncryptionService -import com.bitchat.android.model.* -import com.bitchat.android.protocol.* +import com.bitchat.android.mesh.MeshCore +import com.bitchat.android.mesh.MeshService +import com.bitchat.android.mesh.MeshTransport +import com.bitchat.android.mesh.PeerInfo +import com.bitchat.android.model.BitchatFilePacket +import com.bitchat.android.model.RoutedPacket +import com.bitchat.android.protocol.BitchatPacket import com.bitchat.android.service.TransportBridgeService import com.bitchat.android.sync.GossipSyncManager import com.bitchat.android.util.toHexString -// Mesh-layer components are reused from the existing Bluetooth stack -import com.bitchat.android.mesh.PeerManager -import com.bitchat.android.mesh.PeerManagerDelegate -import com.bitchat.android.mesh.PeerInfo -import com.bitchat.android.mesh.FragmentManager -import com.bitchat.android.mesh.SecurityManager -import com.bitchat.android.mesh.SecurityManagerDelegate -import com.bitchat.android.mesh.StoreForwardManager -import com.bitchat.android.mesh.StoreForwardManagerDelegate -import com.bitchat.android.mesh.MessageHandler -import com.bitchat.android.mesh.MessageHandlerDelegate -import com.bitchat.android.mesh.PacketProcessor -import com.bitchat.android.mesh.PacketProcessorDelegate -import kotlinx.coroutines.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch import java.io.IOException import java.net.Inet6Address import java.net.ServerSocket import java.net.Socket import java.nio.ByteBuffer import java.nio.ByteOrder -import java.util.UUID import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.Executors @@ -52,7 +49,7 @@ import java.util.concurrent.Executors * - MessageHandler: Message type processing and relay logic * - PacketProcessor: Incoming packet routing */ -class WifiAwareMeshService(private val context: Context) : TransportBridgeService.TransportLayer { +class WifiAwareMeshService(private val context: Context) : MeshService, TransportBridgeService.TransportLayer { companion object { private const val TAG = "WifiAwareMeshService" @@ -65,18 +62,10 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic private val encryptionService = EncryptionService(context) // Peer ID must match BluetoothMeshService: first 16 hex chars of identity fingerprint (8 bytes) - val myPeerID: String = encryptionService.getIdentityFingerprint().take(16) - - // Core components - private val peerManager = PeerManager() - private val fragmentManager = FragmentManager() - private val securityManager = SecurityManager(encryptionService, myPeerID) - private val storeForwardManager = StoreForwardManager() - private val messageHandler = MessageHandler(myPeerID, context.applicationContext) - private val packetProcessor = PacketProcessor(myPeerID) - - // Gossip sync - private val gossipSyncManager: GossipSyncManager + override val myPeerID: String = encryptionService.getIdentityFingerprint().take(16) + private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + private val wifiTransport = WifiAwareTransport() + private lateinit var meshCore: MeshCore // Wi-Fi Aware transport private val awareManager = context.getSystemService(WifiAwareManager::class.java) @@ -87,10 +76,13 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic private var isActive = false // Delegate - var delegate: WifiAwareMeshDelegate? = null - - // Coroutines - must be initialized before tracker - private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + override var delegate: WifiAwareMeshDelegate? = null + set(value) { + field = value + if (::meshCore.isInitialized) { + meshCore.delegate = value + } + } private val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager // Transport state @@ -102,59 +94,31 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic private val lastTimestamps = ConcurrentHashMap() init { - setupDelegates() - messageHandler.packetProcessor = packetProcessor - - // Use shared GossipSyncManager from MeshServiceHolder if available (minimal refactor) val shared = com.bitchat.android.service.MeshServiceHolder.sharedGossipSyncManager - if (shared != null) { - gossipSyncManager = shared - } else { - gossipSyncManager = GossipSyncManager( - myPeerID = myPeerID, - scope = serviceScope, - configProvider = object : GossipSyncManager.ConfigProvider { - override fun seenCapacity(): Int = 500 - override fun gcsMaxBytes(): Int = 400 - override fun gcsTargetFpr(): Double = 0.01 + meshCore = MeshCore( + context = context.applicationContext, + scope = serviceScope, + transport = wifiTransport, + encryptionService = encryptionService, + myPeerID = myPeerID, + maxTtl = MAX_TTL, + sharedGossipManager = shared, + gossipConfigProvider = object : GossipSyncManager.ConfigProvider { + override fun seenCapacity(): Int = 500 + override fun gcsMaxBytes(): Int = 400 + override fun gcsTargetFpr(): Double = 0.01 + }, + hooks = MeshCore.Hooks( + onAnnounceProcessed = { routed, _ -> + routed.peerID?.let { pid -> + try { meshCore.gossipSyncManager.scheduleInitialSyncToPeer(pid, 1_000) } catch (_: Exception) { } + } + }, + leavePayloadProvider = { + (delegate?.getNickname() ?: myPeerID).toByteArray(Charsets.UTF_8) } ) - gossipSyncManager.delegate = object : GossipSyncManager.Delegate { - override fun sendPacketToPeer(peerID: String, packet: BitchatPacket) { - this@WifiAwareMeshService.sendPacketToPeer(peerID, packet) - } - override fun sendPacket(packet: BitchatPacket) { - dispatchGlobal(RoutedPacket(packet)) - } - override fun signPacketForBroadcast(packet: BitchatPacket): BitchatPacket { - return signPacketBeforeBroadcast(packet) - } - } - } - } - - /** - * Helper method hexToBa. - */ - private fun hexStringToByteArray(hex: String): ByteArray { - val out = ByteArray(8) - var idx = 0 - var s = hex - while (s.length >= 2 && idx < 8) { - val b = s.substring(0, 2).toIntOrNull(16)?.toByte() ?: 0 - out[idx++] = b - s = s.drop(2) - } - return out - } - - /** - * Sign packet before broadcasting. - */ - private fun signPacketBeforeBroadcast(packet: BitchatPacket): BitchatPacket { - val data = packet.toBinaryDataForSigning() ?: return packet - val sig = encryptionService.signData(data) ?: return packet - return packet.copy(signature = sig) + ) } /** @@ -177,17 +141,7 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic 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) + meshCore.sendFromBridge(packet) } /** @@ -226,195 +180,7 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic } } - /** - * Configures delegates for internal components so that events are routed back - * through this service and ultimately to the {@link WifiAwareMeshDelegate}. - */ - private fun setupDelegates() { - peerManager.delegate = object : PeerManagerDelegate { - override fun onPeerListUpdated(peerIDs: List) { - delegate?.didUpdatePeerList(peerIDs) - } - override fun onPeerRemoved(peerID: String) { - try { gossipSyncManager.removeAnnouncementForPeer(peerID) } catch (_: Exception) { } - try { encryptionService.removePeer(peerID) } catch (_: Exception) { } - } - } - - securityManager.delegate = object : SecurityManagerDelegate { - override fun onKeyExchangeCompleted(peerID: String, peerPublicKeyData: ByteArray) { - serviceScope.launch { - delay(100) - sendAnnouncementToPeer(peerID) - delay(1000) - storeForwardManager.sendCachedMessages(peerID) - } - } - override fun sendHandshakeResponse(peerID: String, response: ByteArray) { - val packet = BitchatPacket( - version = 1u, - type = MessageType.NOISE_HANDSHAKE.value, - senderID = hexStringToByteArray(myPeerID), - recipientID = hexStringToByteArray(peerID), - timestamp = System.currentTimeMillis().toULong(), - payload = response, - ttl = MAX_TTL - ) - dispatchGlobal(RoutedPacket(signPacketBeforeBroadcast(packet))) - } - override fun getPeerInfo(peerID: String): PeerInfo? { - return peerManager.getPeerInfo(peerID) - } - } - - storeForwardManager.delegate = object : StoreForwardManagerDelegate { - override fun isFavorite(peerID: String) = delegate?.isFavorite(peerID) ?: false - override fun isPeerOnline(peerID: String) = peerManager.isPeerActive(peerID) - override fun sendPacket(packet: BitchatPacket) { - dispatchGlobal(RoutedPacket(packet)) - } - } - - messageHandler.delegate = object : MessageHandlerDelegate { - override fun addOrUpdatePeer(peerID: String, nickname: String) = - peerManager.addOrUpdatePeer(peerID, nickname) - override fun removePeer(peerID: String) = peerManager.removePeer(peerID) - override fun updatePeerNickname(peerID: String, nickname: String) { - peerManager.addOrUpdatePeer(peerID, nickname) - } - override fun getPeerNickname(peerID: String) = - peerManager.getPeerNickname(peerID) - override fun getNetworkSize() = peerManager.getActivePeerCount() - override fun getMyNickname() = delegate?.getNickname() - override fun getPeerInfo(peerID: String): PeerInfo? = peerManager.getPeerInfo(peerID) - override fun updatePeerInfo( - peerID: String, - nickname: String, - noisePublicKey: ByteArray, - signingPublicKey: ByteArray, - isVerified: Boolean - ): Boolean = peerManager.updatePeerInfo(peerID, nickname, noisePublicKey, signingPublicKey, isVerified) - - override fun sendPacket(packet: BitchatPacket) { - dispatchGlobal(RoutedPacket(signPacketBeforeBroadcast(packet))) - } - override fun relayPacket(routed: RoutedPacket) { dispatchGlobal(routed) } - override fun getBroadcastRecipient() = SpecialRecipients.BROADCAST - - override fun verifySignature(packet: BitchatPacket, peerID: String) = - securityManager.verifySignature(packet, peerID) - override fun encryptForPeer(data: ByteArray, recipientPeerID: String) = - securityManager.encryptForPeer(data, recipientPeerID) - override fun decryptFromPeer(encryptedData: ByteArray, senderPeerID: String) = - securityManager.decryptFromPeer(encryptedData, senderPeerID) - override fun verifyEd25519Signature(signature: ByteArray, data: ByteArray, publicKey: ByteArray): Boolean = - encryptionService.verifyEd25519Signature(signature, data, publicKey) - - override fun hasNoiseSession(peerID: String) = - encryptionService.hasEstablishedSession(peerID) - override fun initiateNoiseHandshake(peerID: String) { - serviceScope.launch { - val hs = encryptionService.initiateHandshake(peerID) ?: return@launch - val packet = BitchatPacket( - version = 1u, - type = MessageType.NOISE_HANDSHAKE.value, - senderID = hexStringToByteArray(myPeerID), - recipientID = hexStringToByteArray(peerID), - timestamp = System.currentTimeMillis().toULong(), - payload = hs, - ttl = MAX_TTL - ) - dispatchGlobal(RoutedPacket(signPacketBeforeBroadcast(packet))) - } - } - override fun processNoiseHandshakeMessage(payload: ByteArray, peerID: String): ByteArray? = - try { encryptionService.processHandshakeMessage(payload, peerID) } catch (_: Exception) { null } - - override fun updatePeerIDBinding(newPeerID: String, nickname: String, publicKey: ByteArray, previousPeerID: String?) { - peerManager.addOrUpdatePeer(newPeerID, nickname) - val fingerprint = peerManager.storeFingerprintForPeer(newPeerID, publicKey) - previousPeerID?.let { peerManager.removePeer(it) } - Log.d(TAG, "Updated peer binding to $newPeerID, fp=${fingerprint.take(16)}") - } - - override fun decryptChannelMessage(encryptedContent: ByteArray, channel: String) = - delegate?.decryptChannelMessage(encryptedContent, channel) - - override fun onMessageReceived(message: BitchatMessage) { - delegate?.didReceiveMessage(message) - } - override fun onChannelLeave(channel: String, fromPeer: String) { - delegate?.didReceiveChannelLeave(channel, fromPeer) - } - override fun onDeliveryAckReceived(messageID: String, peerID: String) { - delegate?.didReceiveDeliveryAck(messageID, peerID) - } - override fun onReadReceiptReceived(messageID: String, peerID: String) { - delegate?.didReceiveReadReceipt(messageID, peerID) - } - } - - packetProcessor.delegate = object : PacketProcessorDelegate { - override fun validatePacketSecurity(packet: BitchatPacket, peerID: String) = - securityManager.validatePacket(packet, peerID) - override fun updatePeerLastSeen(peerID: String) = peerManager.updatePeerLastSeen(peerID) - override fun getPeerNickname(peerID: String): String? = peerManager.getPeerNickname(peerID) - override fun getNetworkSize(): Int = peerManager.getActivePeerCount() - override fun getBroadcastRecipient(): ByteArray = SpecialRecipients.BROADCAST - - override fun handleNoiseHandshake(routed: RoutedPacket): Boolean = - runBlocking { securityManager.handleNoiseHandshake(routed) } - - override fun handleNoiseEncrypted(routed: RoutedPacket) { - serviceScope.launch { messageHandler.handleNoiseEncrypted(routed) } - } - - override fun handleAnnounce(routed: RoutedPacket) { - serviceScope.launch { - val isFirst = messageHandler.handleAnnounce(routed) - routed.peerID?.let { pid -> - try { gossipSyncManager.scheduleInitialSyncToPeer(pid, 1_000) } catch (_: Exception) { } - } - try { gossipSyncManager.onPublicPacketSeen(routed.packet) } catch (_: Exception) { } - } - } - - override fun handleMessage(routed: RoutedPacket) { - serviceScope.launch { messageHandler.handleMessage(routed) } - try { - val pkt = routed.packet - val isBroadcast = (pkt.recipientID == null || pkt.recipientID.contentEquals(SpecialRecipients.BROADCAST)) - if (isBroadcast && pkt.type == MessageType.MESSAGE.value) { - gossipSyncManager.onPublicPacketSeen(pkt) - } - } catch (_: Exception) { } - } - - override fun handleLeave(routed: RoutedPacket) { - serviceScope.launch { messageHandler.handleLeave(routed) } - } - - override fun handleFragment(packet: BitchatPacket): BitchatPacket? { - try { - val isBroadcast = (packet.recipientID == null || packet.recipientID.contentEquals(SpecialRecipients.BROADCAST)) - if (isBroadcast && packet.type == MessageType.FRAGMENT.value) { - gossipSyncManager.onPublicPacketSeen(packet) - } - } catch (_: Exception) { } - return fragmentManager.handleFragment(packet) - } - - override fun sendAnnouncementToPeer(peerID: String) = this@WifiAwareMeshService.sendAnnouncementToPeer(peerID) - override fun sendCachedMessages(peerID: String) = storeForwardManager.sendCachedMessages(peerID) - override fun relayPacket(routed: RoutedPacket) = dispatchGlobal(routed) - - override fun handleRequestSync(routed: RoutedPacket) { - val fromPeer = routed.peerID ?: return - val req = RequestSyncPacket.decode(routed.packet.payload) ?: return - gossipSyncManager.handleRequestSync(fromPeer, req) - } - } - } + /** * Starts Wi-Fi Aware services (publish + subscribe). @@ -428,7 +194,7 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic Manifest.permission.ACCESS_WIFI_STATE, Manifest.permission.CHANGE_WIFI_STATE ]) - fun startServices() { + override fun startServices() { if (isActive) return isActive = true Log.i(TAG, "Starting Wi-Fi Aware mesh with peer ID: $myPeerID") @@ -527,10 +293,9 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic } }, Handler(Looper.getMainLooper())) - sendPeriodicBroadcastAnnounce() + meshCore.startCore() startPeriodicConnectionMaintenance() connectionTracker.start() - gossipSyncManager.start() // Register with cross-layer transport bridge TransportBridgeService.register("WIFI", this) @@ -539,7 +304,7 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic /** * Stops the Wi-Fi Aware mesh services and cleans up sockets and sessions. */ - fun stopServices() { + override fun stopServices() { if (!isActive) return isActive = false Log.i(TAG, "Stopping Wi-Fi Aware mesh") @@ -547,12 +312,12 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic // Unregister from bridge TransportBridgeService.unregister("WIFI") - sendLeaveAnnouncement() + meshCore.sendLeaveAnnouncement() serviceScope.launch { delay(200) - gossipSyncManager.stop() + meshCore.stopCore() connectionTracker.stop() // Handles socket closing and callback unregistration publishSession?.close(); publishSession = null @@ -561,29 +326,12 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic handleToPeerId.clear() - peerManager.shutdown() - fragmentManager.shutdown() - securityManager.shutdown() - storeForwardManager.shutdown() - messageHandler.shutdown() - packetProcessor.shutdown() + meshCore.shutdown() serviceScope.cancel() } } - /** - * Periodically broadcasts an ANNOUNCE packet (every ~30s) while the service is active, - * so new/idle peers can discover us without user action. - */ - private fun sendPeriodicBroadcastAnnounce() { - serviceScope.launch { - while (isActive) { - try { delay(30_000); sendBroadcastAnnounce() } catch (_: Exception) { } - } - } - } - /** * Periodic active maintenance: retries connections to discovered but unconnected peers. */ @@ -681,13 +429,13 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic client.keepAlive = true Log.d(TAG, "SERVER: accepted TCP from ${peerId.take(8)} addr=${client.inetAddress?.hostAddress}") connectionTracker.onClientConnected(peerId, client) - try { peerManager.setDirectConnection(peerId, true) } catch (_: Exception) {} - try { peerManager.addOrUpdatePeer(peerId, peerId) } catch (_: Exception) {} + try { meshCore.setDirectConnection(peerId, true) } catch (_: Exception) {} + try { meshCore.addOrUpdatePeer(peerId, peerId) } catch (_: Exception) {} listenerExec.execute { listenToPeer(client, peerId) } handleSubscriberKeepAlive(client, peerId, pubSession, peerHandle) // Kick off Noise handshake for this logical peer if (myPeerID < peerId) { - messageHandler.delegate?.initiateNoiseHandshake(peerId) + meshCore.initiateNoiseHandshake(peerId) Log.d(TAG, "SERVER: initiating Noise handshake to ${peerId.take(8)} (lower ID)") } // Ensure fast presence even before handshake settles @@ -817,13 +565,13 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic Log.d(TAG, "CLIENT: TCP connected to ${peerId.take(8)} addr=$scopedAddr:$port (iface=$iface)") connectionTracker.onClientConnected(peerId, sock) - try { peerManager.setDirectConnection(peerId, true) } catch (_: Exception) {} - try { peerManager.addOrUpdatePeer(peerId, peerId) } catch (_: Exception) {} + try { meshCore.setDirectConnection(peerId, true) } catch (_: Exception) {} + try { meshCore.addOrUpdatePeer(peerId, peerId) } catch (_: Exception) {} listenerExec.execute { listenToPeer(sock, peerId) } handleServerKeepAlive(sock, peerId, peerHandle) // Kick off Noise handshake for this logical peer if (myPeerID < peerId) { - messageHandler.delegate?.initiateNoiseHandshake(peerId) + meshCore.initiateNoiseHandshake(peerId) Log.d(TAG, "CLIENT: initiating Noise handshake to ${peerId.take(8)} (lower ID)") } // Ensure fast presence even before handshake settles @@ -908,7 +656,7 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic // 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})") - packetProcessor.processPacket(RoutedPacket(pkt, senderPeerHex, initialLogicalPeerId)) + meshCore.processIncoming(pkt, senderPeerHex, initialLogicalPeerId) } // Breaking out of the loop means the socket is dead or service is stopping. @@ -922,7 +670,7 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic Log.d(TAG, "Cleaning up peer: $initialId") connectionTracker.disconnect(initialId) - peerManager.removePeer(initialId) + meshCore.removePeer(initialId) } } @@ -932,24 +680,8 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic * @param mentions Optional list of mentioned peer IDs * @param channel Optional channel name */ - fun sendMessage(content: String, mentions: List = emptyList(), channel: String? = null) { - if (content.isEmpty()) return - - serviceScope.launch { - val packet = BitchatPacket( - version = 1u, - type = MessageType.MESSAGE.value, - senderID = hexStringToByteArray(myPeerID), - recipientID = SpecialRecipients.BROADCAST, - timestamp = System.currentTimeMillis().toULong(), - payload = content.toByteArray(Charsets.UTF_8), - signature = null, - ttl = MAX_TTL - ) - val signed = signPacketBeforeBroadcast(packet) - dispatchGlobal(RoutedPacket(signed)) - try { gossipSyncManager.onPublicPacketSeen(signed) } catch (_: Exception) { } - } + override fun sendMessage(content: String, mentions: List, channel: String?) { + meshCore.sendMessage(content, mentions, channel) } /** @@ -960,37 +692,8 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic * @param recipientNickname Recipient nickname * @param messageID Optional message ID (UUID if null) */ - fun sendPrivateMessage(content: String, recipientPeerID: String, recipientNickname: String, messageID: String? = null) { - if (content.isEmpty() || recipientPeerID.isEmpty()) return - - serviceScope.launch { - val finalId = messageID ?: UUID.randomUUID().toString() - - if (encryptionService.hasEstablishedSession(recipientPeerID)) { - try { - val pm = PrivateMessagePacket(messageID = finalId, content = content) - val tlv = pm.encode() ?: return@launch - val payload = NoisePayload(type = NoisePayloadType.PRIVATE_MESSAGE, data = tlv).encode() - val enc = encryptionService.encrypt(payload, recipientPeerID) - - val pkt = BitchatPacket( - version = 1u, - type = MessageType.NOISE_ENCRYPTED.value, - senderID = hexStringToByteArray(myPeerID), - recipientID = hexStringToByteArray(recipientPeerID), - timestamp = System.currentTimeMillis().toULong(), - payload = enc, - signature = null, - ttl = MAX_TTL - ) - dispatchGlobal(RoutedPacket(signPacketBeforeBroadcast(pkt))) - } catch (e: Exception) { - Log.e(TAG, "Failed to encrypt private message: ${e.message}") - } - } else { - messageHandler.delegate?.initiateNoiseHandshake(recipientPeerID) - } - } + override fun sendPrivateMessage(content: String, recipientPeerID: String, recipientNickname: String, messageID: String?) { + meshCore.sendPrivateMessage(content, recipientPeerID, recipientNickname, messageID) } /** @@ -1001,29 +704,8 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic * @param recipientPeerID The peer to notify. * @param readerNickname Nickname of the reader (may be shown by the receiver). */ - fun sendReadReceipt(messageID: String, recipientPeerID: String, readerNickname: String) { - serviceScope.launch { - try { - val payload = NoisePayload( - type = NoisePayloadType.READ_RECEIPT, - data = messageID.toByteArray(Charsets.UTF_8) - ).encode() - val enc = encryptionService.encrypt(payload, recipientPeerID) - val pkt = BitchatPacket( - version = 1u, - type = MessageType.NOISE_ENCRYPTED.value, - senderID = hexStringToByteArray(myPeerID), - recipientID = hexStringToByteArray(recipientPeerID), - timestamp = System.currentTimeMillis().toULong(), - payload = enc, - signature = null, - ttl = MAX_TTL - ) - dispatchGlobal(RoutedPacket(signPacketBeforeBroadcast(pkt))) - } catch (e: Exception) { - Log.e(TAG, "Failed to send read receipt: ${e.message}") - } - } + override fun sendReadReceipt(messageID: String, recipientPeerID: String, readerNickname: String) { + meshCore.sendReadReceipt(messageID, recipientPeerID, readerNickname) } /** @@ -1032,28 +714,8 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic * * @param file Encoded metadata and chunks descriptor of the file to send. */ - fun sendFileBroadcast(file: BitchatFilePacket) { - try { - val payload = file.encode() ?: run { Log.e(TAG, "file TLV encode failed"); return } - serviceScope.launch { - val pkt = BitchatPacket( - version = 2u, // FILE_TRANSFER big length - type = MessageType.FILE_TRANSFER.value, - senderID = hexStringToByteArray(myPeerID), - recipientID = SpecialRecipients.BROADCAST, - timestamp = System.currentTimeMillis().toULong(), - payload = payload, - signature = null, - ttl = MAX_TTL - ) - val signed = signPacketBeforeBroadcast(pkt) - val transferId = sha256Hex(payload) - dispatchGlobal(RoutedPacket(signed, transferId = transferId)) - try { gossipSyncManager.onPublicPacketSeen(signed) } catch (_: Exception) { } - } - } catch (e: Exception) { - Log.e(TAG, "sendFileBroadcast failed: ${e.message}", e) - } + override fun sendFileBroadcast(file: BitchatFilePacket) { + meshCore.sendFileBroadcast(file) } /** @@ -1063,33 +725,8 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic * @param recipientPeerID Target peer. * @param file Encoded metadata and chunks descriptor of the file to send. */ - fun sendFilePrivate(recipientPeerID: String, file: BitchatFilePacket) { - try { - serviceScope.launch { - if (!encryptionService.hasEstablishedSession(recipientPeerID)) { - messageHandler.delegate?.initiateNoiseHandshake(recipientPeerID) - return@launch - } - val tlv = file.encode() ?: return@launch - val np = NoisePayload(type = NoisePayloadType.FILE_TRANSFER, data = tlv).encode() - val enc = encryptionService.encrypt(np, recipientPeerID) - val pkt = BitchatPacket( - version = 1u, - type = MessageType.NOISE_ENCRYPTED.value, - senderID = hexStringToByteArray(myPeerID), - recipientID = hexStringToByteArray(recipientPeerID), - timestamp = System.currentTimeMillis().toULong(), - payload = enc, - signature = null, - ttl = MAX_TTL - ) - val signed = signPacketBeforeBroadcast(pkt) - val transferId = sha256Hex(tlv) - dispatchGlobal(RoutedPacket(signed, transferId = transferId)) - } - } catch (e: Exception) { - Log.e(TAG, "sendFilePrivate failed: ${e.message}", e) - } + override fun sendFilePrivate(recipientPeerID: String, file: BitchatFilePacket) { + meshCore.sendFilePrivate(recipientPeerID, file) } /** @@ -1098,116 +735,57 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic * @param transferId Deterministic id (usually sha256 of the file TLV). * @return true if a transfer with this id was found and cancellation was scheduled, false otherwise. */ - fun cancelFileTransfer(transferId: String): Boolean { - return false + override fun cancelFileTransfer(transferId: String): Boolean { + return meshCore.cancelFileTransfer(transferId) } - /** - * Computes the SHA-256 of the given bytes and returns a lowercase hex string. - * Falls back to the byte-length in hex if MessageDigest is unavailable. - */ - private fun sha256Hex(bytes: ByteArray): String = try { - val md = java.security.MessageDigest.getInstance("SHA-256") - md.update(bytes); md.digest().joinToString("") { "%02x".format(it) } - } catch (_: Exception) { bytes.size.toString(16) } - /** * Broadcasts an ANNOUNCE packet to the entire mesh. */ - fun sendBroadcastAnnounce() { - serviceScope.launch { - val nickname = delegate?.getNickname() ?: myPeerID - val staticKey = encryptionService.getStaticPublicKey() ?: run { - Log.e(TAG, "No static public key available for announcement"); return@launch - } - val signingKey = encryptionService.getSigningPublicKey() ?: run { - Log.e(TAG, "No signing public key available for announcement"); return@launch - } - - val tlvPayload = IdentityAnnouncement(nickname, staticKey, signingKey).encode() ?: return@launch - - val announcePacket = BitchatPacket( - type = MessageType.ANNOUNCE.value, - ttl = MAX_TTL, - senderID = myPeerID, - payload = tlvPayload - ) - val signed = signPacketBeforeBroadcast(announcePacket) - - dispatchGlobal(RoutedPacket(signed)) - try { gossipSyncManager.onPublicPacketSeen(signed) } catch (_: Exception) { } - } + override fun sendBroadcastAnnounce() { + meshCore.sendBroadcastAnnounce() } /** * Sends an ANNOUNCE packet to a specific peer. */ - fun sendAnnouncementToPeer(peerID: String) { - if (peerManager.hasAnnouncedToPeer(peerID)) return - - val nickname = delegate?.getNickname() ?: myPeerID - val staticKey = encryptionService.getStaticPublicKey() ?: return - val signingKey = encryptionService.getSigningPublicKey() ?: return - - val tlvPayload = IdentityAnnouncement(nickname, staticKey, signingKey).encode() ?: return - - val packet = BitchatPacket( - type = MessageType.ANNOUNCE.value, - ttl = MAX_TTL, - senderID = myPeerID, - payload = tlvPayload - ) - val signed = signPacketBeforeBroadcast(packet) - - dispatchGlobal(RoutedPacket(signed)) - peerManager.markPeerAsAnnouncedTo(peerID) - try { gossipSyncManager.onPublicPacketSeen(signed) } catch (_: Exception) { } - } - - /** - * Sends a LEAVE announcement to all peers before disconnecting. - */ - private fun sendLeaveAnnouncement() { - val nickname = delegate?.getNickname() ?: myPeerID - val packet = BitchatPacket( - type = MessageType.LEAVE.value, - ttl = MAX_TTL, - senderID = myPeerID, - payload = nickname.toByteArray() - ) - dispatchGlobal(RoutedPacket(signPacketBeforeBroadcast(packet))) + override fun sendAnnouncementToPeer(peerID: String) { + meshCore.sendAnnouncementToPeer(peerID) } /** @return Mapping of peer IDs to nicknames. */ - fun getPeerNicknames(): Map = peerManager.getAllPeerNicknames() + override fun getPeerNicknames(): Map = meshCore.getPeerNicknames() /** @return Mapping of peer IDs to RSSI values. */ - fun getPeerRSSI(): Map = peerManager.getAllPeerRSSI() + override fun getPeerRSSI(): Map = meshCore.getPeerRSSI() + + /** @return current active peer count for status surfaces. */ + override fun getActivePeerCount(): Int = meshCore.getActivePeerCount() /** * @return true if a Noise session with the peer is fully established. */ - fun hasEstablishedSession(peerID: String) = encryptionService.hasEstablishedSession(peerID) + override fun hasEstablishedSession(peerID: String) = meshCore.hasEstablishedSession(peerID) /** * @return a human-readable Noise session state for the given peer (implementation-defined). */ - fun getSessionState(peerID: String) = encryptionService.getSessionState(peerID) + override fun getSessionState(peerID: String) = meshCore.getSessionState(peerID) /** * Triggers a Noise handshake with the given peer. Safe to call repeatedly; no-op if already handshaking/established. */ - fun initiateNoiseHandshake(peerID: String) = messageHandler.delegate?.initiateNoiseHandshake(peerID) + override fun initiateNoiseHandshake(peerID: String) = meshCore.initiateNoiseHandshake(peerID) /** * @return the stored public-key fingerprint (hex) for a peer, if known. */ - fun getPeerFingerprint(peerID: String): String? = peerManager.getFingerprintForPeer(peerID) + override fun getPeerFingerprint(peerID: String): String? = meshCore.getPeerFingerprint(peerID) /** * Retrieves the full profile for a peer, including keys and verification state, if available. */ - fun getPeerInfo(peerID: String): PeerInfo? = peerManager.getPeerInfo(peerID) + override fun getPeerInfo(peerID: String): PeerInfo? = meshCore.getPeerInfo(peerID) /** * Updates local metadata for a peer and returns whether the change was applied. @@ -1219,35 +797,35 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic * @param isVerified Whether this identity is verified by the user. * @return true if the record was updated or created, false otherwise. */ - fun updatePeerInfo( + override fun updatePeerInfo( peerID: String, nickname: String, noisePublicKey: ByteArray, signingPublicKey: ByteArray, isVerified: Boolean - ): Boolean = peerManager.updatePeerInfo(peerID, nickname, noisePublicKey, signingPublicKey, isVerified) + ): Boolean = meshCore.updatePeerInfo(peerID, nickname, noisePublicKey, signingPublicKey, isVerified) /** * @return the local device’s long-term identity fingerprint (hex). */ - fun getIdentityFingerprint(): String = encryptionService.getIdentityFingerprint() + override fun getIdentityFingerprint(): String = meshCore.getIdentityFingerprint() /** * @return true if the UI should show an β€œencrypted” indicator for this peer. */ - fun shouldShowEncryptionIcon(peerID: String) = encryptionService.hasEstablishedSession(peerID) + override fun shouldShowEncryptionIcon(peerID: String) = meshCore.shouldShowEncryptionIcon(peerID) /** * @return a snapshot list of peers with established Noise sessions. */ - fun getEncryptedPeers(): List = emptyList() + override fun getEncryptedPeers(): List = meshCore.getEncryptedPeers() /** * @return the current IPv4/IPv6 address of a connected peer, if any. * Prefers the scoped IPv6 address format. */ - fun getDeviceAddressForPeer(peerID: String): String? = - connectionTracker.peerSockets[peerID]?.let { resolveScopedAddress(it) } + override fun getDeviceAddressForPeer(peerID: String): String? = + meshCore.getDeviceAddressForPeer(peerID) /** * Helper to resolve a scoped IPv6 address from a socket for UI display. @@ -1269,18 +847,13 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic * @return a mapping of peerID β†’ connected device IP address for all active sockets. * Results are formatted as scoped addresses if applicable. */ - fun getDeviceAddressToPeerMapping(): Map { - val map = mutableMapOf() - connectionTracker.peerSockets.forEach { (pid, sock) -> - map[pid] = resolveScopedAddress(sock) ?: "unknown" - } - return map - } + override fun getDeviceAddressToPeerMapping(): Map = + meshCore.getDeviceAddressToPeerMapping() /** * @return map of peer ID to nickname, bridged for UI warning fix. */ - fun getPeerNicknamesMap(): Map = peerManager.getAllPeerNicknames() + fun getPeerNicknamesMap(): Map = meshCore.getPeerNicknames() /** Returns recently discovered peer IDs via Aware discovery (may not be connected). */ fun getDiscoveredPeerIds(): Set = @@ -1289,23 +862,27 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic /** * Utility for logs/UI: pretty-prints one peer-to-address mapping per line. */ - fun printDeviceAddressesForPeers(): String = + override fun printDeviceAddressesForPeers(): String = getDeviceAddressToPeerMapping().entries.joinToString("\n") { "${it.key} -> ${it.value}" } /** * @return A detailed string containing the debug status of all mesh components. */ - fun getDebugStatus(): String = buildString { - appendLine("=== Wi-Fi Aware Mesh Debug Status ===") - appendLine("My Peer ID: $myPeerID") - appendLine("Peers: ${connectionTracker.peerSockets.keys}") - appendLine(connectionTracker.getDebugInfo()) - appendLine(peerManager.getDebugInfo(getDeviceAddressToPeerMapping())) - appendLine(fragmentManager.getDebugInfo()) - appendLine(securityManager.getDebugInfo()) - appendLine(storeForwardManager.getDebugInfo()) - appendLine(messageHandler.getDebugInfo()) - appendLine(packetProcessor.getDebugInfo()) + override fun getDebugStatus(): String { + return meshCore.getDebugStatus( + transportInfo = connectionTracker.getDebugInfo(), + deviceMap = getDeviceAddressToPeerMapping(), + extraLines = listOf("Peers: ${connectionTracker.peerSockets.keys}"), + title = "Wi-Fi Aware Mesh Debug Status" + ) + } + + override fun clearAllInternalData() { + meshCore.clearAllInternalData() + } + + override fun clearAllEncryptionData() { + meshCore.clearAllEncryptionData() } /** Utility extension to safely close sockets. */ @@ -1313,20 +890,32 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic /** Utility extension to safely close server sockets. */ private fun ServerSocket.closeQuietly() = try { close() } catch (_: Exception) {} -} + private inner class WifiAwareTransport : MeshTransport { + override val id: String = "WIFI" -/** - * Delegate interface for mesh service callbacks (maintains exact same interface) - */ -interface WifiAwareMeshDelegate { - fun didReceiveMessage(message: BitchatMessage) - fun didUpdatePeerList(peers: List) - fun didReceiveChannelLeave(channel: String, fromPeer: String) - fun didReceiveDeliveryAck(messageID: String, recipientPeerID: String) - fun didReceiveReadReceipt(messageID: String, recipientPeerID: String) - fun decryptChannelMessage(encryptedContent: ByteArray, channel: String): String? - fun getNickname(): String? - fun isFavorite(peerID: String): Boolean - // registerPeerPublicKey REMOVED - fingerprints now handled centrally in PeerManager + override fun broadcastPacket(routed: RoutedPacket) { + this@WifiAwareMeshService.broadcastPacket(routed) + } + + override fun sendPacketToPeer(peerID: String, packet: BitchatPacket) { + this@WifiAwareMeshService.sendPacketToPeer(peerID, packet) + } + + override fun getDeviceAddressForPeer(peerID: String): String? { + return connectionTracker.peerSockets[peerID]?.let { resolveScopedAddress(it) } + } + + override fun getDeviceAddressToPeerMapping(): Map { + val map = mutableMapOf() + connectionTracker.peerSockets.forEach { (pid, sock) -> + map[pid] = resolveScopedAddress(sock) ?: "unknown" + } + return map + } + + override fun getTransportDebugInfo(): String { + return connectionTracker.getDebugInfo() + } + } }