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.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 kotlinx.coroutines.* import java.util.* import kotlin.math.sign import kotlin.random.Random /** * Bluetooth mesh service - REFACTORED to use component-based architecture * 100% compatible with iOS version and maintains exact same UUIDs, packet format, and protocol logic * * This is now a coordinator that orchestrates the following components: * - PeerManager: Peer lifecycle management * - FragmentManager: Message fragmentation and reassembly * - SecurityManager: Security, duplicate detection, encryption * - StoreForwardManager: Offline message caching * - MessageHandler: Message type processing and relay logic * - BluetoothConnectionManager: BLE connections and GATT operations * - PacketProcessor: Incoming packet routing */ class BluetoothMeshService(private val context: Context) { private val debugManager by lazy { try { com.bitchat.android.ui.debug.DebugSettingsManager.getInstance() } catch (e: Exception) { null } } companion object { private const val TAG = "BluetoothMeshService" private const val MAX_TTL: UByte = 7u } // Core components - each handling specific responsibilities 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) internal val connectionManager = BluetoothConnectionManager(context, myPeerID, fragmentManager) // Made internal for access private val packetProcessor = PacketProcessor(myPeerID) private lateinit var gossipSyncManager: GossipSyncManager // Service state management private var isActive = false // Delegate for message callbacks (maintains same interface) var delegate: BluetoothMeshDelegate? = null // Coroutines private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) init { setupDelegates() messageHandler.packetProcessor = packetProcessor //startPeriodicDebugLogging() // Initialize sync manager (needs serviceScope) gossipSyncManager = GossipSyncManager( myPeerID = myPeerID, scope = serviceScope, configProvider = object : GossipSyncManager.ConfigProvider { override fun seenCapacity(): Int = try { com.bitchat.android.ui.debug.DebugPreferenceManager.getSeenPacketCapacity(500) } catch (_: Exception) { 500 } override fun gcsMaxBytes(): Int = try { com.bitchat.android.ui.debug.DebugPreferenceManager.getGcsMaxFilterBytes(400) } catch (_: Exception) { 400 } override fun gcsTargetFpr(): Double = try { com.bitchat.android.ui.debug.DebugPreferenceManager.getGcsFprPercent(1.0) / 100.0 } catch (_: Exception) { 0.01 } } ) // Wire sync manager delegate gossipSyncManager.delegate = object : GossipSyncManager.Delegate { override fun sendPacket(packet: BitchatPacket) { connectionManager.broadcastPacket(RoutedPacket(packet)) } override fun sendPacketToPeer(peerID: String, packet: BitchatPacket) { connectionManager.sendPacketToPeer(peerID, packet) } override fun signPacketForBroadcast(packet: BitchatPacket): BitchatPacket { return signPacketBeforeBroadcast(packet) } } } /** * Start periodic debug logging every 10 seconds */ private fun startPeriodicDebugLogging() { serviceScope.launch { while (isActive) { try { delay(10000) // 10 seconds if (isActive) { // Double-check before logging val debugInfo = getDebugStatus() Log.d(TAG, "=== PERIODIC DEBUG STATUS ===\n$debugInfo\n=== END DEBUG STATUS ===") } } catch (e: Exception) { Log.e(TAG, "Error in periodic debug logging: ${e.message}") } } } } /** * Send broadcast announcement every 30 seconds */ private fun sendPeriodicBroadcastAnnounce() { serviceScope.launch { while (isActive) { try { delay(30000) // 30 seconds sendBroadcastAnnounce() } catch (e: Exception) { Log.e(TAG, "Error in periodic broadcast announce: ${e.message}") } } } } /** * Setup delegate connections between components */ private fun setupDelegates() { // Provide nickname resolver to BLE broadcaster for detailed logs try { connectionManager.setNicknameResolver { pid -> peerManager.getPeerNickname(pid) } } catch (_: Exception) { } // PeerManager delegates to main mesh service delegate peerManager.delegate = object : PeerManagerDelegate { override fun onPeerListUpdated(peerIDs: List) { 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 { 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) connectionManager.broadcastPacket(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) { connectionManager.broadcastPacket(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) connectionManager.broadcastPacket(RoutedPacket(signedPacket)) } override fun relayPacket(routed: RoutedPacket) { connectionManager.broadcastPacket(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) connectionManager.broadcastPacket(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) { 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 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) { // Only set mapping if not already mapped if (!connectionManager.addressPeerMap.containsKey(deviceAddress)) { connectionManager.addressPeerMap[deviceAddress] = pid Log.d(TAG, "Mapped device $deviceAddress to peer $pid on ANNOUNCE") // Mark this peer as directly connected for UI try { peerManager.getPeerInfo(pid)?.let { // Set direct connection flag // (This will also trigger a peer list update) peerManager.setDirectConnection(pid, true) // Also push reactive directness state to UI (best-effort) try { // Note: UI observes via didUpdatePeerList, but we can also update ChatState on a timer } catch (_: Exception) { } } } catch (_: Exception) { } // Schedule initial sync for this new directly connected peer only 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? { 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) { connectionManager.broadcastPacket(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?) { packetProcessor.processPacket(RoutedPacket(packet, peerID, device?.address)) } override fun onDeviceConnected(device: android.bluetooth.BluetoothDevice) { // Send initial announcements after services are ready serviceScope.launch { delay(200) sendBroadcastAnnounce() } // Verbose debug: device connected try { val addr = device.address val peer = connectionManager.addressPeerMap[addr] val nick = peer?.let { peerManager.getPeerNickname(it) } ?: "unknown" com.bitchat.android.ui.debug.DebugSettingsManager.getInstance() .logPeerConnection(peer ?: "unknown", nick, addr, isInbound = !connectionManager.isClientConnection(addr)!!) } catch (_: Exception) { } } override fun onDeviceDisconnected(device: android.bluetooth.BluetoothDevice) { 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) { } } // Verbose debug: device disconnected try { val nick = peerManager.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) } } } } /** * Start the mesh service */ fun startServices() { // Prevent double starts (defensive programming) if (isActive) { Log.w(TAG, "Mesh service already active, ignoring duplicate start request") return } Log.i(TAG, "Starting Bluetooth mesh service with peer ID: $myPeerID") 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() } else { Log.e(TAG, "Failed to start Bluetooth services") } } /** * Stop all mesh services */ fun stopServices() { if (!isActive) { Log.w(TAG, "Mesh service not active, ignoring stop request") return } Log.i(TAG, "Stopping Bluetooth mesh service") isActive = false // Send leave announcement sendLeaveAnnouncement() serviceScope.launch { delay(200) // Give leave message time to send // Stop all components gossipSyncManager.stop() connectionManager.stopServices() peerManager.shutdown() fragmentManager.shutdown() securityManager.shutdown() storeForwardManager.shutdown() messageHandler.shutdown() packetProcessor.shutdown() serviceScope.cancel() } } /** * 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) connectionManager.broadcastPacket(RoutedPacket(signedPacket)) // Track our own broadcast message for sync try { gossipSyncManager.onPublicPacketSeen(signedPacket) } catch (_: Exception) { } } } /** * 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) connectionManager.broadcastPacket(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 } } } /** * 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 { // 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 = 7u // Same TTL as iOS messageTTL ) // Sign the packet before broadcasting val signedPacket = signPacketBeforeBroadcast(packet) connectionManager.broadcastPacket(RoutedPacket(signedPacket)) Log.d(TAG, "📤 Sent read receipt to $recipientPeerID for message $messageID") } catch (e: Exception) { Log.e(TAG, "Failed to send read receipt to $recipientPeerID: ${e.message}") } } } /** * Send broadcast announce with TLV-encoded identity announcement - exactly like iOS */ fun sendBroadcastAnnounce() { Log.d(TAG, "Sending broadcast announce") serviceScope.launch { val nickname = delegate?.getNickname() ?: 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 connectionManager.broadcastPacket(RoutedPacket(signedPacket)) Log.d(TAG, "Sent iOS-compatible signed TLV announce (${tlvPayload.size} bytes)") // Track announce for sync try { gossipSyncManager.onPublicPacketSeen(signedPacket) } catch (_: Exception) { } } } /** * Send announcement to specific peer with TLV-encoded identity announcement - exactly like iOS */ fun sendAnnouncementToPeer(peerID: String) { if (peerManager.hasAnnouncedToPeer(peerID)) return val nickname = delegate?.getNickname() ?: 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 connectionManager.broadcastPacket(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) { } } /** * Send leave announcement */ private fun sendLeaveAnnouncement() { val nickname = delegate?.getNickname() ?: myPeerID val packet = BitchatPacket( type = MessageType.LEAVE.value, ttl = MAX_TTL, senderID = myPeerID, payload = nickname.toByteArray() ) // Sign the packet before broadcasting val signedPacket = signPacketBeforeBroadcast(packet) connectionManager.broadcastPacket(RoutedPacket(signedPacket)) } /** * Get peer nicknames */ fun getPeerNicknames(): Map = peerManager.getAllPeerNicknames() /** * Get peer RSSI values */ fun getPeerRSSI(): Map = peerManager.getAllPeerRSSI() /** * Check if we have an established Noise session with a peer */ fun hasEstablishedSession(peerID: String): Boolean { return encryptionService.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) } /** * 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) } /** * Get peer fingerprint for identity management */ fun getPeerFingerprint(peerID: String): String? { return peerManager.getFingerprintForPeer(peerID) } /** * Get peer info for verification purposes */ fun getPeerInfo(peerID: String): PeerInfo? { return peerManager.getPeerInfo(peerID) } /** * Update peer information with verification data */ fun updatePeerInfo( peerID: String, nickname: String, noisePublicKey: ByteArray, signingPublicKey: ByteArray, isVerified: Boolean ): Boolean { return peerManager.updatePeerInfo(peerID, nickname, noisePublicKey, signingPublicKey, isVerified) } /** * Get our identity fingerprint */ fun getIdentityFingerprint(): String { return encryptionService.getIdentityFingerprint() } /** * Check if encryption icon should be shown for a peer */ fun shouldShowEncryptionIcon(peerID: String): Boolean { return encryptionService.hasEstablishedSession(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() } /** * Get device address for a specific peer ID */ fun getDeviceAddressForPeer(peerID: String): String? { return connectionManager.addressPeerMap.entries.find { it.value == peerID }?.key } /** * Get all device addresses mapped to their peer IDs */ fun getDeviceAddressToPeerMapping(): Map { return connectionManager.addressPeerMap.toMap() } /** * Print device addresses for all connected peers */ fun printDeviceAddressesForPeers(): String { return peerManager.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 } } // MARK: - Panic Mode Support /** * Clear all internal mesh service data (for panic mode) */ 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() Log.d(TAG, "✅ Cleared all mesh service internal data") } catch (e: Exception) { Log.e(TAG, "❌ Error clearing mesh service internal data: ${e.message}") } } /** * Clear all encryption and cryptographic data (for panic mode) */ fun clearAllEncryptionData() { Log.w(TAG, "🚨 Clearing all encryption data") try { // Clear encryption service persistent identity (includes Ed25519 signing keys) encryptionService.clearPersistentIdentity() 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 }