mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-08-08 06:46:11 +00:00
1595 lines
63 KiB
Kotlin
1595 lines
63 KiB
Kotlin
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
|
|
import com.bitchat.android.model.DeliveryAck
|
|
import com.bitchat.android.model.ReadReceipt
|
|
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
|
|
*/
|
|
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<String, BluetoothDevice>()
|
|
private val deviceCharacteristics = ConcurrentHashMap<BluetoothDevice, BluetoothGattCharacteristic>()
|
|
private val subscribedDevices = CopyOnWriteArrayList<BluetoothDevice>()
|
|
private val gattConnections = ConcurrentHashMap<BluetoothDevice, BluetoothGatt>() // NEW: Track GATT connections
|
|
private val peripheralRSSI = ConcurrentHashMap<String, Int>() // Track RSSI by device address during discovery
|
|
|
|
// Peer management
|
|
private val peerNicknames = ConcurrentHashMap<String, String>()
|
|
private val activePeers = ConcurrentHashMap<String, Long>() // peerID -> lastSeen timestamp
|
|
private val peerRSSI = ConcurrentHashMap<String, Int>()
|
|
|
|
// Message processing
|
|
private val encryptionService = EncryptionService(context)
|
|
private val processedMessages = Collections.synchronizedSet(mutableSetOf<String>())
|
|
private val processedKeyExchanges = Collections.synchronizedSet(mutableSetOf<String>())
|
|
|
|
// 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<StoredMessage>())
|
|
private val favoriteMessageQueue = ConcurrentHashMap<String, MutableList<StoredMessage>>()
|
|
private val deliveredMessages = Collections.synchronizedSet(mutableSetOf<String>())
|
|
private val cachedMessagesSentToPeer = Collections.synchronizedSet(mutableSetOf<String>())
|
|
|
|
// Fragment handling
|
|
private val incomingFragments = ConcurrentHashMap<String, MutableMap<Int, ByteArray>>()
|
|
private val fragmentMetadata = ConcurrentHashMap<String, Triple<UByte, Int, Long>>() // originalType, totalFragments, timestamp
|
|
|
|
// Privacy and security
|
|
private val announcedToPeers = Collections.synchronizedSet(mutableSetOf<String>())
|
|
private val announcedPeers = Collections.synchronizedSet(mutableSetOf<String>())
|
|
private val blockedUsers = Collections.synchronizedSet(mutableSetOf<String>())
|
|
|
|
// My peer identification - FIXED to match iOS format
|
|
val myPeerID: String = generateCompatiblePeerID()
|
|
|
|
// Delegate for message callbacks
|
|
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) }
|
|
}
|
|
|
|
/**
|
|
* Start the mesh service - begins advertising and scanning - IMPROVED with better initialization
|
|
*/
|
|
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")
|
|
}
|
|
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Failed to start Bluetooth mesh services: ${e.message}")
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Stop all mesh services - FIXED to properly clean up all connections
|
|
*/
|
|
fun stopServices() {
|
|
Log.i(TAG, "Stopping Bluetooth mesh service")
|
|
|
|
// Send leave announcement before disconnecting
|
|
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()
|
|
|
|
serviceScope.cancel()
|
|
}
|
|
}
|
|
|
|
private fun hasBluetoothPermissions(): Boolean {
|
|
val permissions = mutableListOf<String>()
|
|
|
|
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<ScanResult>) {
|
|
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<String>()
|
|
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<Byte>()
|
|
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
|
|
*/
|
|
fun sendMessage(content: String, mentions: List<String> = emptyList(), channel: String? = null) {
|
|
if (content.isEmpty()) return
|
|
|
|
serviceScope.launch {
|
|
val nickname = delegate?.getNickname() ?: myPeerID
|
|
|
|
val message = BitchatMessage(
|
|
sender = nickname,
|
|
content = content,
|
|
timestamp = Date(),
|
|
isRelay = false,
|
|
senderPeerID = myPeerID,
|
|
mentions = if (mentions.isNotEmpty()) mentions else null,
|
|
channel = channel
|
|
)
|
|
|
|
message.toBinaryPayload()?.let { messageData ->
|
|
// Sign the message
|
|
val signature = try {
|
|
encryptionService.sign(messageData)
|
|
} catch (e: Exception) {
|
|
null
|
|
}
|
|
|
|
val packet = BitchatPacket(
|
|
type = MessageType.MESSAGE.value,
|
|
senderID = myPeerID.toByteArray(),
|
|
recipientID = SpecialRecipients.BROADCAST,
|
|
timestamp = System.currentTimeMillis().toULong(),
|
|
payload = messageData,
|
|
signature = signature,
|
|
ttl = MAX_TTL
|
|
)
|
|
|
|
// Add random delay and send
|
|
delay(Random.nextLong(50, 500))
|
|
broadcastPacket(packet)
|
|
|
|
// Single retry for reliability
|
|
delay(300 + Random.nextLong(0, 200))
|
|
broadcastPacket(packet)
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Send private message
|
|
*/
|
|
fun sendPrivateMessage(content: String, recipientPeerID: String, recipientNickname: String, messageID: String? = null) {
|
|
if (content.isEmpty() || recipientPeerID.isEmpty() || recipientNickname.isEmpty()) return
|
|
|
|
serviceScope.launch {
|
|
val nickname = delegate?.getNickname() ?: myPeerID
|
|
|
|
val message = BitchatMessage(
|
|
id = messageID ?: UUID.randomUUID().toString(),
|
|
sender = nickname,
|
|
content = content,
|
|
timestamp = Date(),
|
|
isRelay = false,
|
|
isPrivate = true,
|
|
recipientNickname = recipientNickname,
|
|
senderPeerID = myPeerID
|
|
)
|
|
|
|
message.toBinaryPayload()?.let { messageData ->
|
|
try {
|
|
// Pad and encrypt
|
|
val blockSize = MessagePadding.optimalBlockSize(messageData.size)
|
|
val paddedData = MessagePadding.pad(messageData, blockSize)
|
|
val encryptedPayload = encryptionService.encrypt(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)
|
|
}
|
|
}
|
|
|
|
// Send with delay
|
|
delay(Random.nextLong(50, 500))
|
|
broadcastPacket(packet)
|
|
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Failed to send private message: ${e.message}")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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<StoredMessage>()
|
|
|
|
// 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
|
|
*/
|
|
fun sendBroadcastAnnounce() {
|
|
serviceScope.launch {
|
|
val nickname = delegate?.getNickname() ?: myPeerID
|
|
|
|
val announcePacket = BitchatPacket(
|
|
type = MessageType.ANNOUNCE.value,
|
|
ttl = 3u,
|
|
senderID = myPeerID,
|
|
payload = nickname.toByteArray()
|
|
)
|
|
|
|
// Send multiple times for reliability
|
|
delay(Random.nextLong(0, 500))
|
|
broadcastPacket(announcePacket)
|
|
|
|
delay(500 + Random.nextLong(0, 500))
|
|
broadcastPacket(announcePacket)
|
|
|
|
delay(1000 + Random.nextLong(0, 500))
|
|
broadcastPacket(announcePacket)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Send announcement to specific peer
|
|
*/
|
|
private fun sendAnnouncementToPeer(peerID: String) {
|
|
if (announcedToPeers.contains(peerID)) return
|
|
|
|
val nickname = delegate?.getNickname() ?: myPeerID
|
|
val packet = BitchatPacket(
|
|
type = MessageType.ANNOUNCE.value,
|
|
ttl = 3u,
|
|
senderID = myPeerID,
|
|
payload = nickname.toByteArray()
|
|
)
|
|
|
|
broadcastPacket(packet)
|
|
announcedToPeers.add(peerID)
|
|
}
|
|
|
|
/**
|
|
* Send leave announcement
|
|
*/
|
|
private fun sendLeaveAnnouncement() {
|
|
val nickname = delegate?.getNickname() ?: myPeerID
|
|
val packet = BitchatPacket(
|
|
type = MessageType.LEAVE.value,
|
|
ttl = 1u,
|
|
senderID = myPeerID,
|
|
payload = nickname.toByteArray()
|
|
)
|
|
|
|
broadcastPacket(packet)
|
|
}
|
|
|
|
/**
|
|
* Get peer nicknames
|
|
*/
|
|
fun getPeerNicknames(): Map<String, String> = peerNicknames.toMap()
|
|
|
|
/**
|
|
* Get peer RSSI values
|
|
*/
|
|
fun getPeerRSSI(): Map<String, Int> = peerRSSI.toMap()
|
|
|
|
/**
|
|
* Get debug status information - ADDED for troubleshooting
|
|
*/
|
|
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()
|
|
appendLine("GATT Connections: ${gattConnections.size}")
|
|
gattConnections.keys.forEach { device ->
|
|
appendLine(" - ${device.address}")
|
|
}
|
|
appendLine()
|
|
appendLine("Subscribed Devices: ${subscribedDevices.size}")
|
|
subscribedDevices.forEach { device ->
|
|
appendLine(" - ${device.address}")
|
|
}
|
|
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()
|
|
appendLine("Peer RSSI Values: ${peerRSSI.size}")
|
|
peerRSSI.forEach { (peerID, rssi) ->
|
|
appendLine(" - $peerID: $rssi dBm")
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Notify delegate of peer list updates
|
|
*/
|
|
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()
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delegate interface for mesh service callbacks
|
|
*/
|
|
interface BluetoothMeshDelegate {
|
|
fun didReceiveMessage(message: BitchatMessage)
|
|
fun didConnectToPeer(peerID: String)
|
|
fun didDisconnectFromPeer(peerID: String)
|
|
fun didUpdatePeerList(peers: List<String>)
|
|
fun didReceiveChannelLeave(channel: String, fromPeer: String)
|
|
fun didReceiveDeliveryAck(ack: DeliveryAck)
|
|
fun didReceiveReadReceipt(receipt: ReadReceipt)
|
|
fun decryptChannelMessage(encryptedContent: ByteArray, channel: String): String?
|
|
fun getNickname(): String?
|
|
fun isFavorite(peerID: String): Boolean
|
|
}
|