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 cf0326e2..065f775e 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt @@ -1,13 +1,7 @@ package com.bitchat.android.mesh -import android.Manifest -import android.bluetooth.* -import android.bluetooth.le.* import android.content.Context -import android.content.pm.PackageManager -import android.os.ParcelUuid import android.util.Log -import androidx.core.app.ActivityCompat import com.bitchat.android.crypto.EncryptionService import com.bitchat.android.crypto.MessagePadding import com.bitchat.android.model.BitchatMessage @@ -17,1133 +11,291 @@ import com.bitchat.android.protocol.BitchatPacket import com.bitchat.android.protocol.MessageType import com.bitchat.android.protocol.SpecialRecipients import kotlinx.coroutines.* -import java.security.MessageDigest import java.util.* -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.CopyOnWriteArrayList import kotlin.random.Random /** - * Bluetooth mesh service - 100% compatible with iOS version - * Uses exact same UUIDs, packet format, and protocol logic + * 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) { companion object { - // Exact same UUIDs as iOS version - private val SERVICE_UUID = UUID.fromString("F47B5E2D-4A9E-4C5A-9B3F-8E1D2C3A4B5C") - private val CHARACTERISTIC_UUID = UUID.fromString("A1B2C3D4-E5F6-4A5B-8C9D-0E1F2A3B4C5D") - private const val TAG = "BluetoothMeshService" private const val MAX_TTL: UByte = 7u - private const val MAX_FRAGMENT_SIZE = 500 // Optimized for BLE 5.0 - private const val MESSAGE_CACHE_TIMEOUT = 43200000L // 12 hours for regular peers - private const val MAX_CACHED_MESSAGES = 100 // For regular peers - private const val MAX_CACHED_MESSAGES_FAVORITES = 1000 // For favorites } - // Core networking components - private val bluetoothManager: BluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager - private val bluetoothAdapter: BluetoothAdapter? = bluetoothManager.adapter - private val bleScanner: BluetoothLeScanner? = bluetoothAdapter?.bluetoothLeScanner - private val bleAdvertiser: BluetoothLeAdvertiser? = bluetoothAdapter?.bluetoothLeAdvertiser - - // Services for peripheral mode - private var gattServer: BluetoothGattServer? = null - private var characteristic: BluetoothGattCharacteristic? = null - - // Connected devices tracking - FIXED to properly track both server and client connections - private val connectedDevices = ConcurrentHashMap() - private val deviceCharacteristics = ConcurrentHashMap() - private val subscribedDevices = CopyOnWriteArrayList() - private val gattConnections = ConcurrentHashMap() // NEW: Track GATT connections - private val peripheralRSSI = ConcurrentHashMap() // Track RSSI by device address during discovery - - // Peer management - private val peerNicknames = ConcurrentHashMap() - private val activePeers = ConcurrentHashMap() // peerID -> lastSeen timestamp - private val peerRSSI = ConcurrentHashMap() - - // Message processing - private val encryptionService = EncryptionService(context) - private val processedMessages = Collections.synchronizedSet(mutableSetOf()) - private val processedKeyExchanges = Collections.synchronizedSet(mutableSetOf()) - - // Store-and-forward message cache - private data class StoredMessage( - val packet: BitchatPacket, - val timestamp: Long, - val messageID: String, - val isForFavorite: Boolean - ) - private val messageCache = Collections.synchronizedList(mutableListOf()) - private val favoriteMessageQueue = ConcurrentHashMap>() - private val deliveredMessages = Collections.synchronizedSet(mutableSetOf()) - private val cachedMessagesSentToPeer = Collections.synchronizedSet(mutableSetOf()) - - // Fragment handling - private val incomingFragments = ConcurrentHashMap>() - private val fragmentMetadata = ConcurrentHashMap>() // originalType, totalFragments, timestamp - - // Privacy and security - private val announcedToPeers = Collections.synchronizedSet(mutableSetOf()) - private val announcedPeers = Collections.synchronizedSet(mutableSetOf()) - private val blockedUsers = Collections.synchronizedSet(mutableSetOf()) - - // My peer identification - FIXED to match iOS format + // My peer identification - same format as iOS val myPeerID: String = generateCompatiblePeerID() - // Delegate for message callbacks + // Core components - each handling specific responsibilities + private val encryptionService = EncryptionService(context) + private val peerManager = PeerManager() + private val fragmentManager = FragmentManager() + private val securityManager = SecurityManager(encryptionService, myPeerID) + private val storeForwardManager = StoreForwardManager() + private val messageHandler = MessageHandler(myPeerID) + private val connectionManager = BluetoothConnectionManager(context, myPeerID) + private val packetProcessor = PacketProcessor(myPeerID) + + // Delegate for message callbacks (maintains same interface) var delegate: BluetoothMeshDelegate? = null // Coroutines private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) - // FIXED: Generate peer ID compatible with iOS - private fun generateCompatiblePeerID(): String { - val randomBytes = ByteArray(4) - Random.nextBytes(randomBytes) - return randomBytes.joinToString("") { "%02x".format(it) } + init { + setupDelegates() } /** - * Start the mesh service - begins advertising and scanning - IMPROVED with better initialization + * Setup delegate connections between components */ - fun startServices() { - Log.i(TAG, "Starting Bluetooth mesh service...") - - if (!hasBluetoothPermissions()) { - Log.e(TAG, "Missing Bluetooth permissions - cannot start services") - return - } - - if (bluetoothAdapter?.isEnabled != true) { - Log.e(TAG, "Bluetooth is not enabled - cannot start services") - return - } - - if (bleScanner == null || bleAdvertiser == null) { - Log.e(TAG, "BLE scanner or advertiser not available") - return - } - - Log.i(TAG, "Starting Bluetooth mesh service with peer ID: $myPeerID") - - // Start services in sequence with proper error handling - try { - setupGattServer() - - // Small delay to ensure GATT server is ready - serviceScope.launch { - delay(500) - - startAdvertising() - delay(200) - - startScanning() - delay(200) - - // Send initial announcements after all services are ready - delay(1000) - sendBroadcastAnnounce() - - // Start periodic cleanup - startPeriodicTasks() - - Log.i(TAG, "All Bluetooth mesh services started successfully") + private fun setupDelegates() { + // PeerManager delegates to main mesh service delegate + peerManager.delegate = object : PeerManagerDelegate { + override fun onPeerConnected(nickname: String) { + delegate?.didConnectToPeer(nickname) } - } catch (e: Exception) { - Log.e(TAG, "Failed to start Bluetooth mesh services: ${e.message}") + override fun onPeerDisconnected(nickname: String) { + delegate?.didDisconnectFromPeer(nickname) + } + + override fun onPeerListUpdated(peerIDs: List) { + delegate?.didUpdatePeerList(peerIDs) + } + } + + // SecurityManager delegate for key exchange notifications + securityManager.delegate = object : SecurityManagerDelegate { + override fun onKeyExchangeCompleted(peerID: String) { + // Send announcement and cached messages after key exchange + serviceScope.launch { + delay(100) + sendAnnouncementToPeer(peerID) + + delay(500) + storeForwardManager.sendCachedMessages(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(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() + } + + // Packet operations + override fun sendPacket(packet: BitchatPacket) { + connectionManager.broadcastPacket(packet) + } + + override fun relayPacket(packet: BitchatPacket) { + connectionManager.broadcastPacket(packet) + } + + 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) + } + + // 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 onPeerDisconnected(nickname: String) { + delegate?.didDisconnectFromPeer(nickname) + } + + override fun onDeliveryAckReceived(ack: DeliveryAck) { + delegate?.didReceiveDeliveryAck(ack) + } + + override fun onReadReceiptReceived(receipt: ReadReceipt) { + delegate?.didReceiveReadReceipt(receipt) + } + } + + // 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 handleKeyExchange(packet: BitchatPacket, peerID: String): Boolean { + return runBlocking { securityManager.handleKeyExchange(packet, peerID) } + } + + override fun handleAnnounce(packet: BitchatPacket, peerID: String) { + serviceScope.launch { messageHandler.handleAnnounce(packet, peerID) } + } + + override fun handleMessage(packet: BitchatPacket, peerID: String) { + serviceScope.launch { messageHandler.handleMessage(packet, peerID) } + } + + override fun handleLeave(packet: BitchatPacket, peerID: String) { + serviceScope.launch { messageHandler.handleLeave(packet, peerID) } + } + + override fun handleFragment(packet: BitchatPacket): BitchatPacket? { + return fragmentManager.handleFragment(packet) + } + + override fun handleDeliveryAck(packet: BitchatPacket, peerID: String) { + serviceScope.launch { messageHandler.handleDeliveryAck(packet, peerID) } + } + + override fun handleReadReceipt(packet: BitchatPacket, peerID: String) { + serviceScope.launch { messageHandler.handleReadReceipt(packet, peerID) } + } + + override fun sendAnnouncementToPeer(peerID: String) { + this@BluetoothMeshService.sendAnnouncementToPeer(peerID) + } + + override fun sendCachedMessages(peerID: String) { + storeForwardManager.sendCachedMessages(peerID) + } + + override fun relayPacket(packet: BitchatPacket) { + connectionManager.broadcastPacket(packet) + } + } + + // BluetoothConnectionManager delegates + connectionManager.delegate = object : BluetoothConnectionManagerDelegate { + override fun onPacketReceived(packet: BitchatPacket, peerID: String, device: android.bluetooth.BluetoothDevice?) { + packetProcessor.processPacket(packet, peerID) + } + + override fun onDeviceConnected(device: android.bluetooth.BluetoothDevice) { + // Send key exchange to newly connected device + serviceScope.launch { + delay(100) // Ensure connection is stable + sendKeyExchangeToDevice() + } + } } } /** - * Stop all mesh services - FIXED to properly clean up all connections + * Start the mesh service + */ + fun startServices() { + Log.i(TAG, "Starting Bluetooth mesh service with peer ID: $myPeerID") + + if (connectionManager.startServices()) { + Log.i(TAG, "Bluetooth services started successfully") + + // Send initial announcements after services are ready + serviceScope.launch { + delay(1000) + sendBroadcastAnnounce() + } + } else { + Log.e(TAG, "Failed to start Bluetooth services") + } + } + + /** + * Stop all mesh services */ fun stopServices() { Log.i(TAG, "Stopping Bluetooth mesh service") - // Send leave announcement before disconnecting + // Send leave announcement sendLeaveAnnouncement() serviceScope.launch { delay(200) // Give leave message time to send - // Cleanup all GATT client connections - gattConnections.values.forEach { gatt -> - try { - gatt.disconnect() - gatt.close() - } catch (e: Exception) { - Log.w(TAG, "Error closing GATT connection: ${e.message}") - } - } - - // Stop advertising and scanning - stopAdvertising() - stopScanning() - - // Close GATT server - gattServer?.close() - - // Clear all connection tracking - connectedDevices.clear() - deviceCharacteristics.clear() - subscribedDevices.clear() - gattConnections.clear() // FIXED: Clear GATT connections - activePeers.clear() + // Stop all components + connectionManager.stopServices() + peerManager.shutdown() + fragmentManager.shutdown() + securityManager.shutdown() + storeForwardManager.shutdown() + messageHandler.shutdown() + packetProcessor.shutdown() serviceScope.cancel() } } - private fun hasBluetoothPermissions(): Boolean { - val permissions = mutableListOf() - - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) { - permissions.addAll(listOf( - Manifest.permission.BLUETOOTH_ADVERTISE, - Manifest.permission.BLUETOOTH_CONNECT, - Manifest.permission.BLUETOOTH_SCAN - )) - } else { - permissions.addAll(listOf( - Manifest.permission.BLUETOOTH, - Manifest.permission.BLUETOOTH_ADMIN - )) - } - - permissions.addAll(listOf( - Manifest.permission.ACCESS_COARSE_LOCATION, - Manifest.permission.ACCESS_FINE_LOCATION - )) - - return permissions.all { - ActivityCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED - } - } - - /** - * Setup GATT server for peripheral mode - */ - private fun setupGattServer() { - if (!hasBluetoothPermissions()) return - - val serverCallback = object : BluetoothGattServerCallback() { - override fun onConnectionStateChange(device: BluetoothDevice, status: Int, newState: Int) { - when (newState) { - BluetoothProfile.STATE_CONNECTED -> { - Log.d(TAG, "Device connected: ${device.address}") - - // FIXED: Check if this is a stale connection from previous app session - if (!subscribedDevices.contains(device)) { - Log.w(TAG, "Received stale connection from ${device.address}, disconnecting") - // Disconnect stale connections immediately - gattServer?.cancelConnection(device) - return - } - - connectedDevices[device.address] = device - } - BluetoothProfile.STATE_DISCONNECTED -> { - Log.d(TAG, "Device disconnected: ${device.address}") - connectedDevices.remove(device.address) - deviceCharacteristics.remove(device) - subscribedDevices.remove(device) - } - } - } - - override fun onCharacteristicWriteRequest( - device: BluetoothDevice, - requestId: Int, - characteristic: BluetoothGattCharacteristic, - preparedWrite: Boolean, - responseNeeded: Boolean, - offset: Int, - value: ByteArray - ) { - if (characteristic.uuid == CHARACTERISTIC_UUID) { - Log.d(TAG, "Received write request from ${device.address}, ${value.size} bytes") - - // Process received packet - val packet = BitchatPacket.fromBinaryData(value) - if (packet != null) { - val peerID = String(packet.senderID).replace("\u0000", "") - handleReceivedPacket(packet, peerID, device) - } - - if (responseNeeded) { - gattServer?.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, null) - } - } - } - - override fun onDescriptorWriteRequest( - device: BluetoothDevice, - requestId: Int, - descriptor: BluetoothGattDescriptor, - preparedWrite: Boolean, - responseNeeded: Boolean, - offset: Int, - value: ByteArray - ) { - if (BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE.contentEquals(value)) { - Log.d(TAG, "Device ${device.address} subscribed to notifications") - if (!subscribedDevices.contains(device)) { - subscribedDevices.add(device) - - // Send key exchange to newly connected device - serviceScope.launch { - delay(100) // Small delay to ensure connection is stable - sendKeyExchangeToDevice(device) - } - } - } - - if (responseNeeded) { - gattServer?.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, null) - } - } - } - - // FIXED: Clean up any existing GATT server to prevent stale connections - gattServer?.close() - - // FIXED: Clear all connection tracking to start fresh - connectedDevices.clear() - deviceCharacteristics.clear() - subscribedDevices.clear() - gattConnections.clear() - - Log.i(TAG, "Setting up fresh GATT server, cleared all previous connections") - - gattServer = bluetoothManager.openGattServer(context, serverCallback) - - // Create characteristic with same UUID as iOS - characteristic = BluetoothGattCharacteristic( - CHARACTERISTIC_UUID, - BluetoothGattCharacteristic.PROPERTY_READ or - BluetoothGattCharacteristic.PROPERTY_WRITE or - BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE or - BluetoothGattCharacteristic.PROPERTY_NOTIFY, - BluetoothGattCharacteristic.PERMISSION_READ or - BluetoothGattCharacteristic.PERMISSION_WRITE - ) - - // Add notification descriptor - val descriptor = BluetoothGattDescriptor( - UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"), - BluetoothGattDescriptor.PERMISSION_READ or BluetoothGattDescriptor.PERMISSION_WRITE - ) - characteristic?.addDescriptor(descriptor) - - // Create service - val service = BluetoothGattService(SERVICE_UUID, BluetoothGattService.SERVICE_TYPE_PRIMARY) - service.addCharacteristic(characteristic) - - gattServer?.addService(service) - } - - /** - * Start BLE advertising - FIXED to exactly match iOS implementation using service data - */ - private fun startAdvertising() { - if (!hasBluetoothPermissions() || bleAdvertiser == null) { - Log.e(TAG, "Cannot start advertising: missing permissions or advertiser unavailable") - return - } - - val settings = AdvertiseSettings.Builder() - .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_BALANCED) - .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM) - .setConnectable(true) - .setTimeout(0) - .build() - - // iOS uses: CBAdvertisementDataLocalNameKey: myPeerID - // Android equivalent: addServiceData() with peer ID bytes - val data = AdvertiseData.Builder() - .addServiceUuid(ParcelUuid(SERVICE_UUID)) - .addServiceData(ParcelUuid(SERVICE_UUID), myPeerID.toByteArray(Charsets.UTF_8)) - .setIncludeTxPowerLevel(false) - .setIncludeDeviceName(false) - .build() - - val advertiseCallback = object : AdvertiseCallback() { - override fun onStartSuccess(settingsInEffect: AdvertiseSettings) { - Log.i(TAG, "Advertising started successfully with peer ID in service data: $myPeerID") - } - - override fun onStartFailure(errorCode: Int) { - val errorMessage = when (errorCode) { - ADVERTISE_FAILED_ALREADY_STARTED -> "Already started" - ADVERTISE_FAILED_DATA_TOO_LARGE -> "Data too large" - ADVERTISE_FAILED_FEATURE_UNSUPPORTED -> "Feature unsupported" - ADVERTISE_FAILED_INTERNAL_ERROR -> "Internal error" - ADVERTISE_FAILED_TOO_MANY_ADVERTISERS -> "Too many advertisers" - else -> "Unknown error: $errorCode" - } - Log.e(TAG, "Advertising failed: $errorMessage") - - // If this fails, try minimal advertising - if (errorCode == ADVERTISE_FAILED_DATA_TOO_LARGE) { - Log.w(TAG, "Service data too large, trying minimal advertising") - startMinimalAdvertising() - } - } - } - - Log.d(TAG, "Starting BLE advertising with peer ID in service data: $myPeerID") - - try { - bleAdvertiser.startAdvertising(settings, data, advertiseCallback) - } catch (e: Exception) { - Log.e(TAG, "Exception starting advertising: ${e.message}") - } - } - - /** - * Fallback minimal advertising without peer ID if the main approach fails - */ - private fun startMinimalAdvertising() { - if (!hasBluetoothPermissions() || bleAdvertiser == null) return - - val settings = AdvertiseSettings.Builder() - .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_BALANCED) - .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM) - .setConnectable(true) - .setTimeout(0) - .build() - - // Minimal advertisement - just the service UUID - val data = AdvertiseData.Builder() - .setIncludeDeviceName(false) - .setIncludeTxPowerLevel(false) - .addServiceUuid(ParcelUuid(SERVICE_UUID)) - .build() - - val advertiseCallback = object : AdvertiseCallback() { - override fun onStartSuccess(settingsInEffect: AdvertiseSettings) { - Log.i(TAG, "Minimal advertising started successfully (no peer ID in advertisement)") - } - - override fun onStartFailure(errorCode: Int) { - Log.e(TAG, "Even minimal advertising failed: $errorCode") - } - } - - try { - bleAdvertiser.startAdvertising(settings, data, advertiseCallback) - } catch (e: Exception) { - Log.e(TAG, "Exception starting minimal advertising: ${e.message}") - } - } - - /** - * Stop BLE advertising - */ - private fun stopAdvertising() { - if (!hasBluetoothPermissions() || bleAdvertiser == null) return - - bleAdvertiser.stopAdvertising(object : AdvertiseCallback() {}) - } - - /** - * Start BLE scanning - FIXED for better peer discovery - */ - private fun startScanning() { - if (!hasBluetoothPermissions() || bleScanner == null) { - Log.e(TAG, "Cannot start scanning: missing permissions or scanner unavailable") - return - } - - val scanFilter = ScanFilter.Builder() - .setServiceUuid(ParcelUuid(SERVICE_UUID)) - .build() - - // FIXED: More aggressive scanning settings for better peer discovery - val scanSettings = ScanSettings.Builder() - .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) // More aggressive scanning - .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES) - .setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE) - .setNumOfMatches(ScanSettings.MATCH_NUM_MAX_ADVERTISEMENT) - .setReportDelay(10) // Report immediately - .build() - - val scanCallback = object : ScanCallback() { - override fun onScanResult(callbackType: Int, result: ScanResult) { - handleScanResult(result) - } - - override fun onBatchScanResults(results: MutableList) { - Log.d(TAG, "Batch scan results: ${results.size} devices") - results.forEach { handleScanResult(it) } - } - - override fun onScanFailed(errorCode: Int) { - val errorMessage = when (errorCode) { - SCAN_FAILED_ALREADY_STARTED -> "Already started" - SCAN_FAILED_APPLICATION_REGISTRATION_FAILED -> "App registration failed" - SCAN_FAILED_FEATURE_UNSUPPORTED -> "Feature unsupported" - SCAN_FAILED_INTERNAL_ERROR -> "Internal error" - else -> "Unknown error: $errorCode" - } - Log.e(TAG, "Scan failed: $errorMessage") - } - } - - try { - bleScanner.startScan(listOf(scanFilter), scanSettings, scanCallback) - Log.i(TAG, "Started BLE scanning with aggressive settings") - } catch (e: Exception) { - Log.e(TAG, "Exception starting scan: ${e.message}") - } - } - - /** - * Stop BLE scanning - */ - private fun stopScanning() { - if (!hasBluetoothPermissions() || bleScanner == null) return - - bleScanner.stopScan(object : ScanCallback() {}) - } - - /** - * Handle scan result - connect to discovered devices - FIXED to connect regardless of peer ID extraction - */ - private fun handleScanResult(result: ScanResult) { - val device = result.device - val rssi = result.rssi - - Log.d(TAG, "Scan result: ${device.address}, RSSI: $rssi") - - // Filter out weak signals - if (rssi < -90) { - Log.d(TAG, "Ignoring weak signal from ${device.address}: $rssi dBm") - return - } - - // Check if already known and connected - if (connectedDevices.values.any { it.address == device.address }) { - Log.d(TAG, "Already connected to device: ${device.address}") - return - } - - // FIXED: Always attempt connection to devices advertising our service UUID - // Peer ID will be obtained during key exchange, not from advertisements - Log.i(TAG, "Found bitchat service at ${device.address} (RSSI: $rssi), attempting connection") - - // Store RSSI by device address for now (will be mapped to peer ID after key exchange) - peripheralRSSI[device.address] = rssi - - // Attempt connection - peer ID will be determined during key exchange - connectToDevice(device) - - // OPTIONAL: Still try to extract peer ID for optimization, but don't require it - var discoveredPeerID: String? = null - - // Method 1: Try service data first (Android method - peer ID is in service data) - val scanRecord = result.scanRecord - val serviceData = scanRecord?.getServiceData(ParcelUuid(SERVICE_UUID)) - if (serviceData != null && serviceData.isNotEmpty()) { - try { - val peerIdFromServiceData = String(serviceData, Charsets.UTF_8) - if (peerIdFromServiceData.length == 8 && peerIdFromServiceData != myPeerID) { - discoveredPeerID = peerIdFromServiceData - Log.d(TAG, "Extracted peer ID from service data: $discoveredPeerID") - } - } catch (e: Exception) { - Log.w(TAG, "Failed to decode service data as peer ID: ${e.message}") - } - } - - // Method 2: Try device name as fallback (iOS method compatibility) - if (discoveredPeerID == null) { - try { - val deviceName = device.name - if (!deviceName.isNullOrEmpty() && deviceName.length == 8 && deviceName != myPeerID) { - discoveredPeerID = deviceName - Log.d(TAG, "Extracted peer ID from device name (iOS compatibility): $discoveredPeerID") - } - } catch (e: SecurityException) { - Log.w(TAG, "No permission to access device name") - } - } - - // If we extracted a peer ID, pre-populate tracking data - if (discoveredPeerID != null) { - Log.i(TAG, "Pre-discovered peer: $discoveredPeerID at ${device.address}") - activePeers[discoveredPeerID] = System.currentTimeMillis() - peerRSSI[discoveredPeerID] = rssi - } else { - Log.d(TAG, "No peer ID found in advertisements for ${device.address}, will get it via key exchange") - } - } - - /** - * Connect to a discovered device - */ - private fun connectToDevice(device: BluetoothDevice) { - if (!hasBluetoothPermissions()) return - - Log.d(TAG, "Connecting to device: ${device.address}") - - val gattCallback = object : BluetoothGattCallback() { - override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) { - when (newState) { - BluetoothProfile.STATE_CONNECTED -> { - Log.d(TAG, "Connected to ${gatt.device.address}") - // FIXED: Properly track the GATT connection - connectedDevices[gatt.device.address] = gatt.device - gattConnections[gatt.device] = gatt - gatt.discoverServices() - } - BluetoothProfile.STATE_DISCONNECTED -> { - Log.d(TAG, "Disconnected from ${gatt.device.address}") - connectedDevices.remove(gatt.device.address) - deviceCharacteristics.remove(gatt.device) - gattConnections.remove(gatt.device) // FIXED: Remove GATT connection tracking - gatt.close() - } - } - } - - override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) { - if (status == BluetoothGatt.GATT_SUCCESS) { - val service = gatt.getService(SERVICE_UUID) - val characteristic = service?.getCharacteristic(CHARACTERISTIC_UUID) - - if (characteristic != null) { - deviceCharacteristics[gatt.device] = characteristic - gatt.setCharacteristicNotification(characteristic, true) - - // Enable notifications - val descriptor = characteristic.getDescriptor( - UUID.fromString("00002902-0000-1000-8000-00805f9b34fb") - ) - descriptor?.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE - gatt.writeDescriptor(descriptor) - - // Send key exchange - serviceScope.launch { - delay(200) // Ensure connection is stable - sendKeyExchangeToGatt(gatt) - } - } - } - } - - override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) { - val value = characteristic.value - Log.d(TAG, "Received notification from ${gatt.device.address}, ${value.size} bytes") - - val packet = BitchatPacket.fromBinaryData(value) - if (packet != null) { - val peerID = String(packet.senderID).replace("\u0000", "") - handleReceivedPacket(packet, peerID, gatt.device) - } - } - - override fun onCharacteristicWrite(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) { - if (status == BluetoothGatt.GATT_SUCCESS) { - Log.d(TAG, "Successfully wrote to characteristic on ${gatt.device.address}") - } else { - Log.w(TAG, "Failed to write to characteristic on ${gatt.device.address}, status: $status") - } - } - } - - device.connectGatt(context, false, gattCallback) - } - - /** - * Send key exchange to a connected device (server mode) - */ - private fun sendKeyExchangeToDevice(device: BluetoothDevice) { - val publicKeyData = encryptionService.getCombinedPublicKeyData() - val packet = BitchatPacket( - type = MessageType.KEY_EXCHANGE.value, - ttl = 1u, - senderID = myPeerID, - payload = publicKeyData - ) - - packet.toBinaryData()?.let { data -> - characteristic?.value = data - gattServer?.notifyCharacteristicChanged(device, characteristic, false) - Log.d(TAG, "Sent key exchange to device: ${device.address}") - } - } - - /** - * Send key exchange to a connected device (client mode) - */ - private fun sendKeyExchangeToGatt(gatt: BluetoothGatt) { - val publicKeyData = encryptionService.getCombinedPublicKeyData() - val packet = BitchatPacket( - type = MessageType.KEY_EXCHANGE.value, - ttl = 1u, - senderID = myPeerID, - payload = publicKeyData - ) - - val characteristic = deviceCharacteristics[gatt.device] - if (characteristic != null) { - packet.toBinaryData()?.let { data -> - characteristic.value = data - gatt.writeCharacteristic(characteristic) - Log.d(TAG, "Sent key exchange to GATT: ${gatt.device.address}") - } - } - } - - /** - * Handle received packet - core protocol logic (exact same as iOS) - */ - private fun handleReceivedPacket(packet: BitchatPacket, peerID: String, device: BluetoothDevice? = null) { - serviceScope.launch { - // TTL check - if (packet.ttl == 0u.toUByte()) return@launch - - // Validate packet payload - if (packet.payload.isEmpty()) return@launch - - // Update last seen timestamp - if (peerID != "unknown" && peerID != myPeerID) { - activePeers[peerID] = System.currentTimeMillis() - } - - // Replay attack protection (same 5-minute window as iOS) - val currentTime = System.currentTimeMillis() - val timeDiff = kotlin.math.abs(currentTime - packet.timestamp.toLong()) - if (timeDiff > 300000) { // 5 minutes - Log.d(TAG, "Dropping old packet from $peerID, time diff: ${timeDiff/1000}s") - return@launch - } - - // Duplicate detection - val messageID = generateMessageID(packet) - if (processedMessages.contains(messageID)) { - return@launch - } - processedMessages.add(messageID) - - // Process based on message type (exact same logic as iOS) - when (MessageType.fromValue(packet.type)) { - MessageType.KEY_EXCHANGE -> handleKeyExchange(packet, peerID, device) - MessageType.ANNOUNCE -> handleAnnounce(packet, peerID) - MessageType.MESSAGE -> handleMessage(packet, peerID) - MessageType.LEAVE -> handleLeave(packet, peerID) - MessageType.FRAGMENT_START, - MessageType.FRAGMENT_CONTINUE, - MessageType.FRAGMENT_END -> handleFragment(packet, peerID) - MessageType.DELIVERY_ACK -> handleDeliveryAck(packet, peerID) - MessageType.READ_RECEIPT -> handleReadReceipt(packet, peerID) - else -> Log.d(TAG, "Unknown message type: ${packet.type}") - } - } - } - - /** - * Generate message ID for duplicate detection - */ - private fun generateMessageID(packet: BitchatPacket): String { - return when (MessageType.fromValue(packet.type)) { - MessageType.FRAGMENT_START, MessageType.FRAGMENT_CONTINUE, MessageType.FRAGMENT_END -> { - "${packet.timestamp}-${String(packet.senderID)}-${packet.type}-${packet.payload.contentHashCode()}" - } - else -> { - "${packet.timestamp}-${String(packet.senderID)}-${packet.payload.sliceArray(0 until minOf(64, packet.payload.size)).contentHashCode()}" - } - } - } - - /** - * Handle key exchange message - */ - private suspend fun handleKeyExchange(packet: BitchatPacket, peerID: String, device: BluetoothDevice?) { - if (peerID == myPeerID) return - - if (packet.payload.isNotEmpty()) { - val exchangeKey = "$peerID-${packet.payload.sliceArray(0 until minOf(16, packet.payload.size)).contentHashCode()}" - - if (processedKeyExchanges.contains(exchangeKey)) return - processedKeyExchanges.add(exchangeKey) - - try { - encryptionService.addPeerPublicKey(peerID, packet.payload) - - // Add to active peers - activePeers[peerID] = System.currentTimeMillis() - - // Send our announce - delay(100) - sendAnnouncementToPeer(peerID) - - // Send cached messages for this peer - delay(500) - sendCachedMessages(peerID) - - Log.d(TAG, "Processed key exchange from $peerID") - - } catch (e: Exception) { - Log.e(TAG, "Failed to process key exchange from $peerID: ${e.message}") - } - } - } - - /** - * Handle announce message - */ - private suspend fun handleAnnounce(packet: BitchatPacket, peerID: String) { - if (peerID == myPeerID) return - - val nickname = String(packet.payload, Charsets.UTF_8) - Log.d(TAG, "Received announce from $peerID: $nickname") - - // Clean up stale peer IDs with the same nickname (exact same logic as iOS) - val stalePeerIDs = mutableListOf() - peerNicknames.forEach { (existingPeerID, existingNickname) -> - if (existingNickname == nickname && existingPeerID != peerID) { - val lastSeen = activePeers[existingPeerID] ?: 0 - val wasRecentlySeen = (System.currentTimeMillis() - lastSeen) < 10000 - if (!wasRecentlySeen) { - stalePeerIDs.add(existingPeerID) - } - } - } - - // Remove stale peer IDs - stalePeerIDs.forEach { stalePeerID -> - peerNicknames.remove(stalePeerID) - activePeers.remove(stalePeerID) - announcedPeers.remove(stalePeerID) - peerRSSI.remove(stalePeerID) - } - - // Add new peer - val isFirstAnnounce = !announcedPeers.contains(peerID) - peerNicknames[peerID] = nickname - activePeers[peerID] = System.currentTimeMillis() - - if (isFirstAnnounce) { - announcedPeers.add(peerID) - delegate?.didConnectToPeer(nickname) - notifyPeerListUpdate() - } - - // Relay announce if TTL > 0 - if (packet.ttl > 1u) { - val relayPacket = packet.copy(ttl = (packet.ttl - 1u).toUByte()) - delay(Random.nextLong(100, 300)) - broadcastPacket(relayPacket) - } - } - - /** - * Handle broadcast or private message - */ - private suspend fun handleMessage(packet: BitchatPacket, peerID: String) { - if (peerID == myPeerID) return - - val recipientID = packet.recipientID?.takeIf { !it.contentEquals(SpecialRecipients.BROADCAST) } - - if (recipientID == null) { - // BROADCAST MESSAGE - try { - // Parse message - val message = BitchatMessage.fromBinaryPayload(packet.payload) - if (message != null) { - // Check for cover traffic (dummy messages) - if (message.content.startsWith("☂DUMMY☂")) { - return // Silently discard - } - - peerNicknames[peerID] = message.sender - - // Handle encrypted channel messages - val finalContent = if (message.channel != null && message.isEncrypted && message.encryptedContent != null) { - delegate?.decryptChannelMessage(message.encryptedContent, message.channel) - ?: "[Encrypted message - password required]" - } else { - message.content - } - - // Replace timestamp with current time - val messageWithCurrentTime = message.copy( - content = finalContent, - senderPeerID = peerID, - timestamp = Date() // Use current time instead of original timestamp - ) - - delegate?.didReceiveMessage(messageWithCurrentTime) - } - - // Relay broadcast messages - relayMessage(packet) - - } catch (e: Exception) { - Log.e(TAG, "Failed to process broadcast message: ${e.message}") - } - - } else if (recipientID != null && String(recipientID).replace("\u0000", "") == myPeerID) { - // PRIVATE MESSAGE FOR US - try { - // Verify signature if present - packet.signature?.let { signature -> - if (!encryptionService.verify(signature, packet.payload, peerID)) { - Log.w(TAG, "Invalid signature for private message from $peerID") - return - } - } - - // Decrypt message - val decryptedData = encryptionService.decrypt(packet.payload, peerID) - val unpaddedData = MessagePadding.unpad(decryptedData) - - // Parse message - val message = BitchatMessage.fromBinaryPayload(unpaddedData) - if (message != null) { - // Check for cover traffic (dummy messages) - if (message.content.startsWith("☂DUMMY☂")) { - return // Silently discard - } - - peerNicknames[peerID] = message.sender - - // Replace timestamp with current time - val messageWithCurrentTime = message.copy( - senderPeerID = peerID, - timestamp = Date() // Use current time instead of original timestamp - ) - delegate?.didReceiveMessage(messageWithCurrentTime) - - // Send delivery ACK - sendDeliveryAck(message, peerID) - } - - } catch (e: Exception) { - Log.e(TAG, "Failed to decrypt private message from $peerID: ${e.message}") - } - - } else if (packet.ttl > 0u) { - // RELAY MESSAGE - relayMessage(packet) - } - } - - /** - * Handle leave message - */ - private suspend fun handleLeave(packet: BitchatPacket, peerID: String) { - val content = String(packet.payload, Charsets.UTF_8) - - if (content.startsWith("#")) { - // Channel leave - delegate?.didReceiveChannelLeave(content, peerID) - } else { - // Peer disconnect - activePeers.remove(peerID) - announcedPeers.remove(peerID) - peerNicknames[peerID]?.let { nickname -> - delegate?.didDisconnectFromPeer(nickname) - } - peerNicknames.remove(peerID) - notifyPeerListUpdate() - } - - // Relay if TTL > 0 - if (packet.ttl > 1u) { - val relayPacket = packet.copy(ttl = (packet.ttl - 1u).toUByte()) - broadcastPacket(relayPacket) - } - } - - /** - * Handle delivery acknowledgment - */ - private suspend fun handleDeliveryAck(packet: BitchatPacket, peerID: String) { - if (packet.recipientID != null && String(packet.recipientID).replace("\u0000", "") == myPeerID) { - try { - val decryptedData = encryptionService.decrypt(packet.payload, peerID) - val ack = DeliveryAck.decode(decryptedData) - if (ack != null) { - delegate?.didReceiveDeliveryAck(ack) - } - } catch (e: Exception) { - Log.e(TAG, "Failed to decrypt delivery ACK: ${e.message}") - } - } else if (packet.ttl > 0u) { - // Relay - val relayPacket = packet.copy(ttl = (packet.ttl - 1u).toUByte()) - broadcastPacket(relayPacket) - } - } - - /** - * Handle read receipt - */ - private suspend fun handleReadReceipt(packet: BitchatPacket, peerID: String) { - if (packet.recipientID != null && String(packet.recipientID).replace("\u0000", "") == myPeerID) { - try { - val decryptedData = encryptionService.decrypt(packet.payload, peerID) - val receipt = ReadReceipt.decode(decryptedData) - if (receipt != null) { - delegate?.didReceiveReadReceipt(receipt) - } - } catch (e: Exception) { - Log.e(TAG, "Failed to decrypt read receipt: ${e.message}") - } - } else if (packet.ttl > 0u) { - // Relay - val relayPacket = packet.copy(ttl = (packet.ttl - 1u).toUByte()) - broadcastPacket(relayPacket) - } - } - - /** - * Handle message fragments - */ - private suspend fun handleFragment(packet: BitchatPacket, peerID: String) { - if (packet.payload.size < 13) return - - try { - // Extract fragment metadata (same format as iOS) - val fragmentIDData = packet.payload.sliceArray(0..7) - val fragmentID = fragmentIDData.contentHashCode().toString() - - val index = ((packet.payload[8].toInt() and 0xFF) shl 8) or (packet.payload[9].toInt() and 0xFF) - val total = ((packet.payload[10].toInt() and 0xFF) shl 8) or (packet.payload[11].toInt() and 0xFF) - val originalType = packet.payload[12].toUByte() - val fragmentData = packet.payload.sliceArray(13 until packet.payload.size) - - // Store fragment - if (!incomingFragments.containsKey(fragmentID)) { - incomingFragments[fragmentID] = mutableMapOf() - fragmentMetadata[fragmentID] = Triple(originalType, total, System.currentTimeMillis()) - } - - incomingFragments[fragmentID]?.put(index, fragmentData) - - // Check if we have all fragments - if (incomingFragments[fragmentID]?.size == total) { - // Reassemble message - val reassembledData = mutableListOf() - for (i in 0 until total) { - incomingFragments[fragmentID]?.get(i)?.let { data -> - reassembledData.addAll(data.asIterable()) - } - } - - // Parse and handle reassembled packet - val reassembledPacket = BitchatPacket.fromBinaryData(reassembledData.toByteArray()) - if (reassembledPacket != null) { - handleReceivedPacket(reassembledPacket, peerID) - } - - // Cleanup - incomingFragments.remove(fragmentID) - fragmentMetadata.remove(fragmentID) - } - - // Relay fragment - if (packet.ttl > 0u) { - val relayPacket = packet.copy(ttl = (packet.ttl - 1u).toUByte()) - broadcastPacket(relayPacket) - } - - } catch (e: Exception) { - Log.e(TAG, "Failed to handle fragment: ${e.message}") - } - - // Clean up old fragments (older than 30 seconds) - val cutoffTime = System.currentTimeMillis() - 30000 - fragmentMetadata.entries.removeAll { (fragmentID, metadata) -> - if (metadata.third < cutoffTime) { - incomingFragments.remove(fragmentID) - true - } else { - false - } - } - } - - /** - * Relay message with adaptive probability (same as iOS) - */ - private suspend fun relayMessage(packet: BitchatPacket) { - if (packet.ttl == 0u.toUByte()) return - - val relayPacket = packet.copy(ttl = (packet.ttl - 1u).toUByte()) - - // Adaptive relay probability based on network size - val networkSize = activePeers.size - val relayProb = when { - networkSize <= 10 -> 1.0 - networkSize <= 30 -> 0.85 - networkSize <= 50 -> 0.7 - networkSize <= 100 -> 0.55 - else -> 0.4 - } - - val shouldRelay = relayPacket.ttl >= 4u || networkSize <= 3 || Random.nextDouble() < relayProb - - if (shouldRelay) { - val delay = Random.nextLong(50, 500) // Random delay like iOS - delay(delay) - broadcastPacket(relayPacket) - } - } - - /** - * Broadcast packet to all connected peers - FIXED to work with both server and client modes - */ - fun broadcastPacket(packet: BitchatPacket) { - val data = packet.toBinaryData() ?: return - - Log.d(TAG, "Broadcasting packet type ${packet.type} to ${subscribedDevices.size} server connections and ${gattConnections.size} client connections") - - // Send to connected devices in server mode (devices connected to our GATT server) - subscribedDevices.forEach { device -> - try { - characteristic?.let { char -> - char.value = data - val success = gattServer?.notifyCharacteristicChanged(device, char, false) ?: false - if (success) { - Log.d(TAG, "Sent packet to server connection: ${device.address}") - } else { - Log.w(TAG, "Failed to send packet to server connection: ${device.address}") - } - } - } catch (e: Exception) { - Log.e(TAG, "Error sending to server connection ${device.address}: ${e.message}") - } - } - - // Send to connected devices in client mode (we are connected to their GATT servers) - gattConnections.forEach { (device, gatt) -> - try { - val characteristic = deviceCharacteristics[device] - if (characteristic != null) { - characteristic.value = data - val success = gatt.writeCharacteristic(characteristic) - if (success) { - Log.d(TAG, "Sent packet to client connection: ${device.address}") - } else { - Log.w(TAG, "Failed to send packet to client connection: ${device.address}") - } - } else { - Log.w(TAG, "No characteristic found for client connection: ${device.address}") - } - } catch (e: Exception) { - Log.e(TAG, "Error sending to client connection ${device.address}: ${e.message}") - } - } - } - /** * Send public message */ @@ -1165,11 +317,7 @@ class BluetoothMeshService(private val context: Context) { message.toBinaryPayload()?.let { messageData -> // Sign the message - val signature = try { - encryptionService.sign(messageData) - } catch (e: Exception) { - null - } + val signature = securityManager.signPacket(messageData) val packet = BitchatPacket( type = MessageType.MESSAGE.value, @@ -1181,13 +329,12 @@ class BluetoothMeshService(private val context: Context) { ttl = MAX_TTL ) - // Add random delay and send + // Send with random delay and retry for reliability delay(Random.nextLong(50, 500)) - broadcastPacket(packet) + connectionManager.broadcastPacket(packet) - // Single retry for reliability delay(300 + Random.nextLong(0, 200)) - broadcastPacket(packet) + connectionManager.broadcastPacket(packet) } } } @@ -1217,37 +364,32 @@ class BluetoothMeshService(private val context: Context) { // Pad and encrypt val blockSize = MessagePadding.optimalBlockSize(messageData.size) val paddedData = MessagePadding.pad(messageData, blockSize) - val encryptedPayload = encryptionService.encrypt(paddedData, recipientPeerID) + val encryptedPayload = securityManager.encryptForPeer(paddedData, recipientPeerID) - // Sign - val signature = try { - encryptionService.sign(encryptedPayload) - } catch (e: Exception) { - null - } - - val packet = BitchatPacket( - type = MessageType.MESSAGE.value, - senderID = myPeerID.toByteArray(), - recipientID = recipientPeerID.toByteArray(), - timestamp = System.currentTimeMillis().toULong(), - payload = encryptedPayload, - signature = signature, - ttl = MAX_TTL - ) - - // Check if recipient is offline and cache for favorites - if (!activePeers.containsKey(recipientPeerID)) { - val isRecipientFavorite = delegate?.isFavorite(recipientPeerID) ?: false - if (isRecipientFavorite) { - cacheMessage(packet, messageID ?: message.id) + if (encryptedPayload != null) { + // Sign + val signature = securityManager.signPacket(encryptedPayload) + + val packet = BitchatPacket( + type = MessageType.MESSAGE.value, + senderID = myPeerID.toByteArray(), + recipientID = recipientPeerID.toByteArray(), + timestamp = System.currentTimeMillis().toULong(), + payload = encryptedPayload, + signature = signature, + ttl = MAX_TTL + ) + + // Cache for offline favorites + if (storeForwardManager.shouldCacheForPeer(recipientPeerID)) { + storeForwardManager.cacheMessage(packet, messageID ?: message.id) } + + // Send with delay + delay(Random.nextLong(50, 500)) + connectionManager.broadcastPacket(packet) } - // Send with delay - delay(Random.nextLong(50, 500)) - broadcastPacket(packet) - } catch (e: Exception) { Log.e(TAG, "Failed to send private message: ${e.message}") } @@ -1255,159 +397,6 @@ class BluetoothMeshService(private val context: Context) { } } - /** - * Cache message for offline delivery - */ - private fun cacheMessage(packet: BitchatPacket, messageID: String) { - // Skip certain message types (same as iOS) - if (packet.type == MessageType.KEY_EXCHANGE.value || - packet.type == MessageType.ANNOUNCE.value || - packet.type == MessageType.LEAVE.value) { - return - } - - // Don't cache broadcast messages - if (packet.recipientID != null && packet.recipientID.contentEquals(SpecialRecipients.BROADCAST)) { - return - } - - val isForFavorite = packet.recipientID?.let { recipientID -> - val recipientPeerID = String(recipientID).replace("\u0000", "") - delegate?.isFavorite(recipientPeerID) ?: false - } ?: false - - val storedMessage = StoredMessage( - packet = packet, - timestamp = System.currentTimeMillis(), - messageID = messageID, - isForFavorite = isForFavorite - ) - - if (isForFavorite && packet.recipientID != null) { - val recipientPeerID = String(packet.recipientID).replace("\u0000", "") - if (!favoriteMessageQueue.containsKey(recipientPeerID)) { - favoriteMessageQueue[recipientPeerID] = mutableListOf() - } - favoriteMessageQueue[recipientPeerID]?.add(storedMessage) - - // Limit favorite queue size - if (favoriteMessageQueue[recipientPeerID]?.size ?: 0 > MAX_CACHED_MESSAGES_FAVORITES) { - favoriteMessageQueue[recipientPeerID]?.removeAt(0) - } - } else { - // Clean up old messages first - cleanupMessageCache() - - messageCache.add(storedMessage) - - // Limit cache size - if (messageCache.size > MAX_CACHED_MESSAGES) { - messageCache.removeAt(0) - } - } - } - - /** - * Send cached messages to peer - */ - private fun sendCachedMessages(peerID: String) { - if (cachedMessagesSentToPeer.contains(peerID)) { - return // Already sent cached messages to this peer - } - - cachedMessagesSentToPeer.add(peerID) - - serviceScope.launch { - cleanupMessageCache() - - val messagesToSend = mutableListOf() - - // Check favorite queue - favoriteMessageQueue[peerID]?.let { favoriteMessages -> - val undeliveredFavorites = favoriteMessages.filter { !deliveredMessages.contains(it.messageID) } - messagesToSend.addAll(undeliveredFavorites) - favoriteMessageQueue.remove(peerID) - } - - // Filter regular cached messages for this recipient - val recipientMessages = messageCache.filter { storedMessage -> - !deliveredMessages.contains(storedMessage.messageID) && - storedMessage.packet.recipientID?.let { recipientID -> - String(recipientID).replace("\u0000", "") == peerID - } == true - } - messagesToSend.addAll(recipientMessages) - - // Sort by timestamp - messagesToSend.sortBy { it.timestamp } - - if (messagesToSend.isNotEmpty()) { - Log.d(TAG, "Sending ${messagesToSend.size} cached messages to $peerID") - } - - // Mark as delivered - val messageIDsToRemove = messagesToSend.map { it.messageID } - deliveredMessages.addAll(messageIDsToRemove) - - // Send with delays - messagesToSend.forEachIndexed { index, storedMessage -> - delay(index * 100L) // 100ms between messages - broadcastPacket(storedMessage.packet) - } - - // Remove sent messages - messageCache.removeAll { messageIDsToRemove.contains(it.messageID) } - } - } - - /** - * Clean up old cached messages - */ - private fun cleanupMessageCache() { - val cutoffTime = System.currentTimeMillis() - MESSAGE_CACHE_TIMEOUT - messageCache.removeAll { !it.isForFavorite && it.timestamp < cutoffTime } - - // Clean up delivered messages set - if (deliveredMessages.size > 1000) { - deliveredMessages.clear() - } - } - - /** - * Send delivery acknowledgment - */ - private fun sendDeliveryAck(message: BitchatMessage, senderPeerID: String) { - serviceScope.launch { - val nickname = delegate?.getNickname() ?: myPeerID - val ack = DeliveryAck( - originalMessageID = message.id, - recipientID = myPeerID, - recipientNickname = nickname, - hopCount = (MAX_TTL - 0u).toUByte() // Placeholder hop count - ) - - try { - val ackData = ack.encode() ?: return@launch - val encryptedPayload = encryptionService.encrypt(ackData, senderPeerID) - - val packet = BitchatPacket( - type = MessageType.DELIVERY_ACK.value, - senderID = myPeerID.toByteArray(), - recipientID = senderPeerID.toByteArray(), - timestamp = System.currentTimeMillis().toULong(), - payload = encryptedPayload, - signature = null, - ttl = 3u - ) - - broadcastPacket(packet) - - } catch (e: Exception) { - Log.e(TAG, "Failed to send delivery ACK: ${e.message}") - } - } - } - /** * Send broadcast announce */ @@ -1424,13 +413,13 @@ class BluetoothMeshService(private val context: Context) { // Send multiple times for reliability delay(Random.nextLong(0, 500)) - broadcastPacket(announcePacket) + connectionManager.broadcastPacket(announcePacket) delay(500 + Random.nextLong(0, 500)) - broadcastPacket(announcePacket) + connectionManager.broadcastPacket(announcePacket) delay(1000 + Random.nextLong(0, 500)) - broadcastPacket(announcePacket) + connectionManager.broadcastPacket(announcePacket) } } @@ -1438,7 +427,7 @@ class BluetoothMeshService(private val context: Context) { * Send announcement to specific peer */ private fun sendAnnouncementToPeer(peerID: String) { - if (announcedToPeers.contains(peerID)) return + if (peerManager.hasAnnouncedToPeer(peerID)) return val nickname = delegate?.getNickname() ?: myPeerID val packet = BitchatPacket( @@ -1448,8 +437,24 @@ class BluetoothMeshService(private val context: Context) { payload = nickname.toByteArray() ) - broadcastPacket(packet) - announcedToPeers.add(peerID) + connectionManager.broadcastPacket(packet) + peerManager.markPeerAsAnnouncedTo(peerID) + } + + /** + * Send key exchange + */ + private fun sendKeyExchangeToDevice() { + val publicKeyData = securityManager.getCombinedPublicKeyData() + val packet = BitchatPacket( + type = MessageType.KEY_EXCHANGE.value, + ttl = 1u, + senderID = myPeerID, + payload = publicKeyData + ) + + connectionManager.broadcastPacket(packet) + Log.d(TAG, "Sent key exchange") } /** @@ -1464,121 +469,55 @@ class BluetoothMeshService(private val context: Context) { payload = nickname.toByteArray() ) - broadcastPacket(packet) + connectionManager.broadcastPacket(packet) } /** * Get peer nicknames */ - fun getPeerNicknames(): Map = peerNicknames.toMap() + fun getPeerNicknames(): Map = peerManager.getAllPeerNicknames() /** - * Get peer RSSI values + * Get peer RSSI values */ - fun getPeerRSSI(): Map = peerRSSI.toMap() + fun getPeerRSSI(): Map = peerManager.getAllPeerRSSI() /** - * Get debug status information - ADDED for troubleshooting + * Get debug status information */ fun getDebugStatus(): String { return buildString { appendLine("=== Bluetooth Mesh Service Debug Status ===") appendLine("My Peer ID: $myPeerID") - appendLine("Bluetooth Enabled: ${bluetoothAdapter?.isEnabled}") - appendLine("Has Permissions: ${hasBluetoothPermissions()}") - appendLine("BLE Scanner Available: ${bleScanner != null}") - appendLine("BLE Advertiser Available: ${bleAdvertiser != null}") - appendLine("GATT Server Active: ${gattServer != null}") appendLine() - appendLine("Connected Devices: ${connectedDevices.size}") - connectedDevices.forEach { (address, device) -> - appendLine(" - $address") - } + appendLine(connectionManager.getDebugInfo()) appendLine() - appendLine("GATT Connections: ${gattConnections.size}") - gattConnections.keys.forEach { device -> - appendLine(" - ${device.address}") - } + appendLine(peerManager.getDebugInfo()) appendLine() - appendLine("Subscribed Devices: ${subscribedDevices.size}") - subscribedDevices.forEach { device -> - appendLine(" - ${device.address}") - } + appendLine(fragmentManager.getDebugInfo()) appendLine() - appendLine("Active Peers: ${activePeers.size}") - activePeers.forEach { (peerID, lastSeen) -> - val nickname = peerNicknames[peerID] ?: "Unknown" - val timeSince = (System.currentTimeMillis() - lastSeen) / 1000 - appendLine(" - $peerID ($nickname) - last seen ${timeSince}s ago") - } + appendLine(securityManager.getDebugInfo()) appendLine() - appendLine("Peer RSSI Values: ${peerRSSI.size}") - peerRSSI.forEach { (peerID, rssi) -> - appendLine(" - $peerID: $rssi dBm") - } + appendLine(storeForwardManager.getDebugInfo()) + appendLine() + appendLine(messageHandler.getDebugInfo()) + appendLine() + appendLine(packetProcessor.getDebugInfo()) } } /** - * Notify delegate of peer list updates + * Generate peer ID compatible with iOS */ - private fun notifyPeerListUpdate() { - val peerList = activePeers.keys.toList().sorted() - delegate?.didUpdatePeerList(peerList) - } - - /** - * Start periodic tasks - */ - private fun startPeriodicTasks() { - // Cleanup stale peers every minute - serviceScope.launch { - while (isActive) { - delay(60000) // 1 minute - cleanupStalePeers() - } - } - - // Reset processed messages every 5 minutes - serviceScope.launch { - while (isActive) { - delay(300000) // 5 minutes - processedMessages.clear() - processedKeyExchanges.clear() - } - } - } - - /** - * Clean up stale peers (same 3-minute threshold as iOS) - */ - private fun cleanupStalePeers() { - val staleThreshold = 180000L // 3 minutes - val now = System.currentTimeMillis() - - val peersToRemove = activePeers.entries.filter { (_, lastSeen) -> - now - lastSeen > staleThreshold - }.map { it.key } - - peersToRemove.forEach { peerID -> - activePeers.remove(peerID) - peerNicknames[peerID]?.let { nickname -> - delegate?.didDisconnectFromPeer(nickname) - } - peerNicknames.remove(peerID) - peerRSSI.remove(peerID) - announcedPeers.remove(peerID) - announcedToPeers.remove(peerID) - } - - if (peersToRemove.isNotEmpty()) { - notifyPeerListUpdate() - } + private fun generateCompatiblePeerID(): String { + val randomBytes = ByteArray(4) + Random.nextBytes(randomBytes) + return randomBytes.joinToString("") { "%02x".format(it) } } } /** - * Delegate interface for mesh service callbacks + * Delegate interface for mesh service callbacks (maintains exact same interface) */ interface BluetoothMeshDelegate { fun didReceiveMessage(message: BitchatMessage)