package com.helible.pilot.controllers import android.Manifest import android.annotation.SuppressLint import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothManager import android.bluetooth.BluetoothSocket import android.content.Context import android.content.IntentFilter import android.content.pm.PackageManager import android.location.LocationManager import android.os.Build import android.util.Log import android.widget.Toast import androidx.activity.ComponentActivity import com.helible.pilot.viewmodels.BluetoothDataTransferService import com.helible.pilot.KMessage import com.helible.pilot.dataclasses.BluetoothDeviceDomain import com.helible.pilot.receivers.BluetoothAdapterStateReceiver import com.helible.pilot.receivers.BluetoothStateReceiver import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import java.io.IOException import java.util.UUID sealed interface ConnectionResult { object ConnectionEstablished : ConnectionResult data class TransferSucceded(val message: String) : ConnectionResult data class Error(val message: String) : ConnectionResult } interface BluetoothController { val isEnabled: StateFlow val isLocationEnabled: StateFlow val isConnected: StateFlow val isScanning: StateFlow val scannedDevices: StateFlow> val pairedDevices: StateFlow> val errors: SharedFlow fun startDiscovery() fun cancelDiscovery() fun connectToDevice(device: String): Flow suspend fun trySendMessage(message: ByteArray): Boolean fun closeConnection() fun onDestroy() } class AndroidBluetoothController(private val context: Context) : BluetoothController { private val bluetoothManager by lazy { context.getSystemService(BluetoothManager::class.java) } private val bluetoothAdapter by lazy { bluetoothManager.adapter } private val locationManager: LocationManager? by lazy { context.getSystemService(ComponentActivity.LOCATION_SERVICE) as LocationManager } private var dataTransferService: BluetoothDataTransferService? = null private val _isConnected: MutableStateFlow = MutableStateFlow(false) override val isConnected: StateFlow get() = _isConnected.asStateFlow() private val _errors = MutableSharedFlow() override val errors: SharedFlow get() = _errors.asSharedFlow() private val _isScanning: MutableStateFlow = MutableStateFlow(false) override val isScanning: StateFlow get() = _isScanning.asStateFlow() private val _isEnabled: MutableStateFlow = MutableStateFlow(false) override val isEnabled: StateFlow get() = _isEnabled.asStateFlow() private val _isLocationEnabled: MutableStateFlow = MutableStateFlow(false) override val isLocationEnabled: StateFlow get() = _isLocationEnabled.asStateFlow() private val _pairedDevices = MutableStateFlow>(emptyList()) override val pairedDevices: StateFlow> get() = _pairedDevices.asStateFlow() private val _scannedDevices: MutableStateFlow> = MutableStateFlow(emptyList()) override val scannedDevices: StateFlow> get() = _scannedDevices.asStateFlow() private var currentClientSocket: BluetoothSocket? = null @SuppressLint("MissingPermission") private val bluetoothAdapterStateReceiver = BluetoothAdapterStateReceiver( onBluetoothEnabledChanged = { isEnabled -> _isEnabled.update { _ -> isEnabled } startDiscovery() Log.i("ScanActivity", "Bluetooth enabled status: $isEnabled") }, onDiscoveryRunningChanged = { isDiscovering -> _isScanning.update { isDiscovering } }, onLocationEnabledChanged = { if (locationManager?.isLocationEnabled == true) { _isLocationEnabled.update { true } } else { _isLocationEnabled.update { false } } } ) @SuppressLint("MissingPermission") private val bluetoothStateReceiver = BluetoothStateReceiver( onDeviceFound = { device, rssi -> if (!hasAllPermissions()) return@BluetoothStateReceiver val newDevice = BluetoothDeviceDomain(device.name ?: "null", device.address, rssi, isScanned = true) _scannedDevices.update { devices -> if (newDevice in devices) devices else devices + newDevice } Log.i( "ScanActivity", "Found new device: ${device.name} ${device.address} $rssi" ) }, onConnectedStateChanged = { isConnected, device -> if (bluetoothAdapter?.bondedDevices?.contains(device) == true) { _isConnected.update { isConnected } } else { CoroutineScope(Dispatchers.IO).launch { _errors.emit("Can't connect to a non-paired device.") } } } ) companion object { // SPP service UUID const val SERVICE_UUID = "00001101-0000-1000-8000-00805F9B34FB" } init { updatePairedDevices() _isEnabled.update { bluetoothAdapter.isEnabled } _isLocationEnabled.update { locationManager?.isLocationEnabled == true } context.registerReceiver( bluetoothAdapterStateReceiver, IntentFilter().apply { addAction(BluetoothAdapter.ACTION_STATE_CHANGED) addAction(BluetoothAdapter.ACTION_DISCOVERY_STARTED) addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED) if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) { addAction(LocationManager.PROVIDERS_CHANGED_ACTION) } } ) context.registerReceiver( bluetoothStateReceiver, IntentFilter().apply { addAction(BluetoothDevice.ACTION_ACL_CONNECTED) addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED) addAction(BluetoothDevice.ACTION_FOUND) } ) } @SuppressLint("MissingPermission") override fun startDiscovery() { if (!hasAllPermissions()) { Toast.makeText(context, "Ошибка: недостаточно разрешений", Toast.LENGTH_SHORT).show() return } if (!_isEnabled.value) { return } if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) { if (locationManager?.isLocationEnabled != true) return } updatePairedDevices() _scannedDevices.update { emptyList() } if (!bluetoothAdapter.isDiscovering) { bluetoothAdapter.startDiscovery() } } @SuppressLint("MissingPermission") override fun cancelDiscovery() { if (!hasAllPermissions()) return if (bluetoothAdapter.isDiscovering) { bluetoothAdapter.cancelDiscovery() } } @SuppressLint("MissingPermission") override fun connectToDevice(device: String): Flow { if (!hasAllPermissions()) { Toast.makeText(context, "Ошибка: нет разрешений", Toast.LENGTH_SHORT).show() return flow {} } return flow { Log.i("BluetoothController", "Connecting to device...") currentClientSocket = bluetoothAdapter.getRemoteDevice(device).createRfcommSocketToServiceRecord( UUID.fromString(SERVICE_UUID) ) currentClientSocket?.let { socket -> try { socket.connect() emit(ConnectionResult.ConnectionEstablished) BluetoothDataTransferService(socket).also { it -> dataTransferService = it emitAll( it.listenForIncomingMessages() .map { ConnectionResult.TransferSucceded(it) } ) } } catch (e: IOException) { socket.close() currentClientSocket = null Log.e("BluetoothController", e.toString()) emit(ConnectionResult.Error("Connection was interrupted")) } } }.onCompletion { closeConnection() }.flowOn(Dispatchers.IO) } override suspend fun trySendMessage(message: ByteArray): Boolean { if (!hasAllPermissions()) { return false } if (dataTransferService == null) { return false } dataTransferService?.sendMessage(message) return true } override fun closeConnection() { currentClientSocket?.close() currentClientSocket = null Log.i("BluetoothController", "Connection closed") } override fun onDestroy() { context.unregisterReceiver(bluetoothAdapterStateReceiver) context.unregisterReceiver(bluetoothStateReceiver) closeConnection() } @SuppressLint("MissingPermission") private fun updatePairedDevices() { if (!hasAllPermissions()) return Log.i("ScanActivity", "${bluetoothAdapter?.bondedDevices}") bluetoothAdapter?.bondedDevices?.onEach { device -> _pairedDevices.update { val currentDevice = BluetoothDeviceDomain( name = device.name ?: "null", macAddress = device.address, rssi = 0, isScanned = false ) if (currentDevice in pairedDevices.value) { pairedDevices.value } else { _pairedDevices.value + currentDevice } } } } private fun hasAllPermissions(): Boolean { val perms = if (Build.VERSION.SDK_INT <= 30) { arrayOf( Manifest.permission.ACCESS_FINE_LOCATION ) } else { arrayOf( Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_CONNECT ) } perms.forEach { perm -> if (context.checkSelfPermission(perm) != PackageManager.PERMISSION_GRANTED) { return false } } return true } }