From 415b5ef0d8520c51a13385b78a4ae54e0768522c Mon Sep 17 00:00:00 2001 From: gogacoder Date: Wed, 27 Sep 2023 21:55:07 +0700 Subject: [PATCH] Paired devices list was added --- ...er.kt => BluetoothAdapterStateReceiver.kt} | 47 +++++--- .../com/helible/pilot/BluetoothController.kt | 113 +++++++++++++----- .../pilot/BluetoothDataTransferService.kt | 48 ++++++++ .../com/helible/pilot/BluetoothUiState.kt | 13 ++ .../com/helible/pilot/BluetoothViewModel.kt | 18 +-- app/src/main/java/com/helible/pilot/Kproto.kt | 23 ++++ .../java/com/helible/pilot/MainViewModel.kt | 14 +-- .../com/helible/pilot/PermissionsRequest.kt | 1 - .../components/BluetoothScannerScreen.kt | 4 +- .../helible/pilot/components/DeviceItem.kt | 49 +++++--- .../pilot/components/DiscoveredDevicesList.kt | 61 +++++++++- 11 files changed, 298 insertions(+), 93 deletions(-) rename app/src/main/java/com/helible/pilot/{BluetoothIntentReceiver.kt => BluetoothAdapterStateReceiver.kt} (66%) create mode 100644 app/src/main/java/com/helible/pilot/BluetoothDataTransferService.kt create mode 100644 app/src/main/java/com/helible/pilot/BluetoothUiState.kt create mode 100644 app/src/main/java/com/helible/pilot/Kproto.kt diff --git a/app/src/main/java/com/helible/pilot/BluetoothIntentReceiver.kt b/app/src/main/java/com/helible/pilot/BluetoothAdapterStateReceiver.kt similarity index 66% rename from app/src/main/java/com/helible/pilot/BluetoothIntentReceiver.kt rename to app/src/main/java/com/helible/pilot/BluetoothAdapterStateReceiver.kt index 4946a61..27f589d 100644 --- a/app/src/main/java/com/helible/pilot/BluetoothIntentReceiver.kt +++ b/app/src/main/java/com/helible/pilot/BluetoothAdapterStateReceiver.kt @@ -9,29 +9,13 @@ import android.content.Intent import android.location.LocationManager import android.os.Build -class BluetoothIntentReceiver( - private val onDeviceFound: (device: BluetoothDevice, rssi: Short) -> Unit, +class BluetoothAdapterStateReceiver( private val onBluetoothEnabledChanged: (isBluetoothEnabled: Boolean) -> Unit, private val onDiscoveryRunningChanged: (isDiscoveryRunning: Boolean) -> Unit, private val onLocationEnabledChanged: () -> Unit ) : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { when (intent?.action) { - BluetoothDevice.ACTION_FOUND -> { - val device = if (Build.VERSION.SDK_INT >= 33) { - intent.getParcelableExtra( - BluetoothDevice.EXTRA_DEVICE, - BluetoothDevice::class.java - ) - } else { - @Suppress("DEPRECATION") intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE) - } - - val rssi = intent.getShortExtra(BluetoothDevice.EXTRA_RSSI, Short.MIN_VALUE) - @SuppressLint("MissingPermission") if (device?.name != null) - onDeviceFound(device, rssi) - } - BluetoothAdapter.ACTION_STATE_CHANGED -> { when (intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1)) { BluetoothAdapter.STATE_ON -> { @@ -56,4 +40,33 @@ class BluetoothIntentReceiver( } } } +} + +class BluetoothStateReceiver( + private val onDeviceFound: (device: BluetoothDevice, rssi: Short) -> Unit, + private val onConnectedStateChanged: (isConnected: Boolean, BluetoothDevice) -> Unit +) : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + val device = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + intent?.getParcelableExtra( + BluetoothDevice.EXTRA_DEVICE, + BluetoothDevice::class.java + ) + } else { + @Suppress("DEPRECATION") intent?.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE) + } + when(intent?.action) { + BluetoothDevice.ACTION_FOUND -> { + val rssi = intent.getShortExtra(BluetoothDevice.EXTRA_RSSI, Short.MIN_VALUE) + @SuppressLint("MissingPermission") if (device?.name != null) + onDeviceFound(device, rssi) + } + BluetoothDevice.ACTION_ACL_CONNECTED -> { + onConnectedStateChanged(true, device ?: return) + } + BluetoothDevice.ACTION_ACL_DISCONNECTED -> { + onConnectedStateChanged(false, device ?: return) + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/helible/pilot/BluetoothController.kt b/app/src/main/java/com/helible/pilot/BluetoothController.kt index fb0c640..cc3d916 100644 --- a/app/src/main/java/com/helible/pilot/BluetoothController.kt +++ b/app/src/main/java/com/helible/pilot/BluetoothController.kt @@ -14,6 +14,7 @@ import android.os.Build import android.util.Log import android.widget.Toast import androidx.activity.ComponentActivity +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow @@ -22,15 +23,20 @@ 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.toSet 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: KMessage): ConnectionResult data class Error(val message: String) : ConnectionResult } @@ -40,12 +46,13 @@ interface BluetoothController { val isConnected: StateFlow val isScanning: StateFlow val scannedDevices: StateFlow> - val pairedDevices: StateFlow> + val pairedDevices: StateFlow> val errors: SharedFlow fun startDiscovery() fun cancelDiscovery() fun connectToDevice(device: Device?): Flow + suspend fun trySendMessage(message: KMessage): KMessage? fun closeConnection() fun onDestroy() } @@ -64,6 +71,8 @@ class AndroidBluetoothController(private val context: Context) : BluetoothContro 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() @@ -84,8 +93,8 @@ class AndroidBluetoothController(private val context: Context) : BluetoothContro override val isLocationEnabled: StateFlow get() = _isLocationEnabled.asStateFlow() - private val _pairedDevices = MutableStateFlow>(emptySet()) - override val pairedDevices: StateFlow> + private val _pairedDevices = MutableStateFlow>(emptyList()) + override val pairedDevices: StateFlow> get() = _pairedDevices.asStateFlow() private val _scannedDevices: MutableStateFlow> = MutableStateFlow(emptyList()) @@ -94,20 +103,8 @@ class AndroidBluetoothController(private val context: Context) : BluetoothContro private var currentClientSocket: BluetoothSocket? = null - @SuppressLint("MissingPermission") - private val bluetoothIntentReceiver = BluetoothIntentReceiver( - onDeviceFound = {device, rssi -> - if(!hasAllPermissions()) return@BluetoothIntentReceiver - val newDevice = Device(device, rssi) - _scannedDevices.update { devices -> - if(newDevice in devices) devices else devices + newDevice - } - Log.i( - "ScanActivity", - "Found new device: ${device.name} ${device.address} $rssi" - ) - }, + private val bluetoothAdapterStateReceiver = BluetoothAdapterStateReceiver( onBluetoothEnabledChanged = { isEnabled -> _isEnabled.update { _ -> isEnabled } startDiscovery() @@ -125,6 +122,30 @@ class AndroidBluetoothController(private val context: Context) : BluetoothContro } ) + @SuppressLint("MissingPermission") + private val bluetoothStateReceiver = BluetoothStateReceiver( + onDeviceFound = { device, rssi -> + if(!hasAllPermissions()) return@BluetoothStateReceiver + val newDevice = Device(device, rssi) + _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 { const val SERVICE_UUID = "af7cc14b-cffa-4a3d-b677-01b0ff0a93d7" } @@ -133,13 +154,26 @@ class AndroidBluetoothController(private val context: Context) : BluetoothContro updatePairedDevices() _isEnabled.update { bluetoothAdapter.isEnabled } _isLocationEnabled.update { locationManager?.isLocationEnabled == true } - context.registerReceiver(bluetoothIntentReceiver, IntentFilter(BluetoothDevice.ACTION_FOUND)) - context.registerReceiver(bluetoothIntentReceiver, IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)) - context.registerReceiver(bluetoothIntentReceiver, IntentFilter(BluetoothAdapter.ACTION_DISCOVERY_STARTED)) - context.registerReceiver(bluetoothIntentReceiver, IntentFilter(BluetoothAdapter.ACTION_DISCOVERY_FINISHED)) - if(Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) { - context.registerReceiver(bluetoothIntentReceiver, IntentFilter(LocationManager.PROVIDERS_CHANGED_ACTION)) - } + 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") @@ -185,6 +219,13 @@ class AndroidBluetoothController(private val context: Context) : BluetoothContro try { socket.connect() emit(ConnectionResult.ConnectionEstablished) + BluetoothDataTransferService(socket).also { it -> + dataTransferService = it + emitAll( + it.listenForIncomingMessages() + .map {ConnectionResult.TransferSucceded(it)} + ) + } } catch (e: IOException) { closeConnection() emit(ConnectionResult.Error("Connection was interrupted")) @@ -193,22 +234,40 @@ class AndroidBluetoothController(private val context: Context) : BluetoothContro }.onCompletion { closeConnection() }.flowOn(Dispatchers.IO) } + override suspend fun trySendMessage(message: KMessage): KMessage? { + if(!hasAllPermissions()){ + return null + } + if(dataTransferService == null) { + return null + } + dataTransferService?.sendMessage(message.toByteArray()) + return message + } + override fun closeConnection() { currentClientSocket?.close() currentClientSocket = null } override fun onDestroy() { - context.unregisterReceiver(bluetoothIntentReceiver) + context.unregisterReceiver(bluetoothAdapterStateReceiver) + context.unregisterReceiver(bluetoothStateReceiver) closeConnection() } @SuppressLint("MissingPermission") private fun updatePairedDevices() { if(!hasAllPermissions()) return - bluetoothAdapter?.bondedDevices.also { devices -> - if(devices != null) { - _pairedDevices.update { devices } + Log.i("ScanActivity", "${bluetoothAdapter?.bondedDevices}") + bluetoothAdapter?.bondedDevices?.onEach { device -> + _pairedDevices.update { + val currentDevice = Device(bluetoothDevice = device, rssi=0, isPaired = true) + if (currentDevice in pairedDevices.value) { + pairedDevices.value + } else { + _pairedDevices.value + currentDevice + } } } } diff --git a/app/src/main/java/com/helible/pilot/BluetoothDataTransferService.kt b/app/src/main/java/com/helible/pilot/BluetoothDataTransferService.kt new file mode 100644 index 0000000..19b214a --- /dev/null +++ b/app/src/main/java/com/helible/pilot/BluetoothDataTransferService.kt @@ -0,0 +1,48 @@ +package com.helible.pilot + +import android.bluetooth.BluetoothSocket +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.withContext + +import java.io.IOException + +class TransferFailedException : IOException("Reading incoming data failed") + +class BluetoothDataTransferService( + private val socket: BluetoothSocket +) { + fun listenForIncomingMessages(): Flow { + return flow { + if(!socket.isConnected) + return@flow + val buffer = ByteArray(128) + while(true) { + val byteCount: Int = try { + socket.inputStream.read(buffer) + } catch (e: IOException) { + throw TransferFailedException() + } + emit( + buffer.decodeToString( + endIndex = byteCount + ).toKMessage() + ) + } + }.flowOn(Dispatchers.IO) + } + + suspend fun sendMessage(bytes: ByteArray): Boolean { + return withContext(Dispatchers.IO) { + try { + socket.outputStream.write(bytes) + } catch (e: IOException) { + e.printStackTrace() + return@withContext false + } + true + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/helible/pilot/BluetoothUiState.kt b/app/src/main/java/com/helible/pilot/BluetoothUiState.kt new file mode 100644 index 0000000..e254fc7 --- /dev/null +++ b/app/src/main/java/com/helible/pilot/BluetoothUiState.kt @@ -0,0 +1,13 @@ +package com.helible.pilot + +import android.bluetooth.BluetoothDevice +data class BluetoothUiState( + val isEnabled: Boolean = false, + val isLocationEnabled: Boolean = false, + val isDiscovering: Boolean = false, + val isConnected: Boolean = false, + val isConnecting: Boolean = false, + val errorMessage: String? = null, + val scannedDevices: List = emptyList(), + val pairedDevices: List = emptyList(), +) \ No newline at end of file diff --git a/app/src/main/java/com/helible/pilot/BluetoothViewModel.kt b/app/src/main/java/com/helible/pilot/BluetoothViewModel.kt index 11d69d1..158d4bf 100644 --- a/app/src/main/java/com/helible/pilot/BluetoothViewModel.kt +++ b/app/src/main/java/com/helible/pilot/BluetoothViewModel.kt @@ -1,7 +1,6 @@ package com.helible.pilot import android.bluetooth.BluetoothDevice -import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Job @@ -16,18 +15,6 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update - -data class BluetoothUiState( - val isEnabled: Boolean = false, - val isLocationEnabled: Boolean = false, - val isDiscovering: Boolean = false, - val isConnected: Boolean = false, - val isConnecting: Boolean = false, - val errorMessage: String? = null, - val scannedDevices: List = emptyList(), - val pairedDevices: List = emptyList(), -) - class BluetoothViewModel( private val bluetoothController: BluetoothController ) : ViewModel() { @@ -41,7 +28,7 @@ class BluetoothViewModel( { scannedDevices, pairedDevices, state -> state.copy( scannedDevices = scannedDevices.toList(), - pairedDevices = pairedDevices.toList() + pairedDevices = pairedDevices ) }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), _state.value) @@ -83,6 +70,9 @@ class BluetoothViewModel( ) } } + is ConnectionResult.TransferSucceded -> { + TODO("Telemetry not implemented") + } is ConnectionResult.Error -> { _state.update { it.copy( isConnected = false, diff --git a/app/src/main/java/com/helible/pilot/Kproto.kt b/app/src/main/java/com/helible/pilot/Kproto.kt new file mode 100644 index 0000000..a4634c4 --- /dev/null +++ b/app/src/main/java/com/helible/pilot/Kproto.kt @@ -0,0 +1,23 @@ +package com.helible.pilot + +// Todo: add checksum +// Todo: add arguments names + +data class KMessage( + val r1: UShort, + val r2: UShort, + val r3: UShort, + val emergStop: Boolean, + val alarm: Boolean +) + +fun KMessage.toByteArray(): ByteArray { + return "$$r1;$r2;$r3;$emergStop;$alarm\r\n".encodeToByteArray() +} + +fun String.toKMessage(): KMessage { + // TODO: implement + return KMessage( + 0u, 0u, 0u, emergStop = false, alarm = false + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/helible/pilot/MainViewModel.kt b/app/src/main/java/com/helible/pilot/MainViewModel.kt index 60b44d9..6a5cfe7 100644 --- a/app/src/main/java/com/helible/pilot/MainViewModel.kt +++ b/app/src/main/java/com/helible/pilot/MainViewModel.kt @@ -1,27 +1,15 @@ package com.helible.pilot import android.bluetooth.BluetoothDevice -import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateListOf -import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope import com.helible.pilot.components.SavedPreferences import com.helible.pilot.components.SavedPreferencesCache -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.flow.update data class Device( val bluetoothDevice: BluetoothDevice, val rssi: Short, + val isPaired: Boolean = false ) class PermissionDialogViewModel: ViewModel() { diff --git a/app/src/main/java/com/helible/pilot/PermissionsRequest.kt b/app/src/main/java/com/helible/pilot/PermissionsRequest.kt index 77cf53c..ad22353 100644 --- a/app/src/main/java/com/helible/pilot/PermissionsRequest.kt +++ b/app/src/main/java/com/helible/pilot/PermissionsRequest.kt @@ -6,7 +6,6 @@ import android.content.Intent import android.content.pm.PackageManager import android.net.Uri import android.provider.Settings -import android.util.Log import androidx.activity.compose.ManagedActivityResultLauncher import androidx.compose.runtime.Composable import androidx.compose.runtime.snapshots.SnapshotStateList diff --git a/app/src/main/java/com/helible/pilot/components/BluetoothScannerScreen.kt b/app/src/main/java/com/helible/pilot/components/BluetoothScannerScreen.kt index 2a02973..cf52c54 100644 --- a/app/src/main/java/com/helible/pilot/components/BluetoothScannerScreen.kt +++ b/app/src/main/java/com/helible/pilot/components/BluetoothScannerScreen.kt @@ -45,7 +45,7 @@ fun BluetoothScannerScreen( val (title, devicesList, controls) = createRefs() Title( - text = "Поиск устройств", + text = "Устройства поблизости", modifier = Modifier .fillMaxWidth() .padding(vertical = 10.dp) @@ -53,7 +53,7 @@ fun BluetoothScannerScreen( ) DiscoveredDevicesList( - devices = bluetoothState.scannedDevices, + bluetoothState = bluetoothState, selectedDevice = selectedDevice, choiceDevice = choiceDevice, modifier = Modifier diff --git a/app/src/main/java/com/helible/pilot/components/DeviceItem.kt b/app/src/main/java/com/helible/pilot/components/DeviceItem.kt index b7039a5..4fe7f1f 100644 --- a/app/src/main/java/com/helible/pilot/components/DeviceItem.kt +++ b/app/src/main/java/com/helible/pilot/components/DeviceItem.kt @@ -29,36 +29,51 @@ import com.helible.pilot.R @SuppressLint("MissingPermission") @Composable -fun DeviceItem(deviceInfo: Device, selectedDevice: Device?, choiceDevice: (device: Device?) -> Unit, modifier: Modifier) { +fun DeviceItem( + deviceInfo: Device, + selectedDevice: Device?, + choiceDevice: (device: Device?) -> Unit, + modifier: Modifier, +) { ElevatedCard( - modifier=modifier.clickable { + modifier = modifier.clickable { choiceDevice(deviceInfo) }, - colors = CardDefaults.elevatedCardColors(containerColor = if (deviceInfo.bluetoothDevice == selectedDevice?.bluetoothDevice) - MaterialTheme.colorScheme.secondaryContainer + colors = CardDefaults.elevatedCardColors( + containerColor = if (deviceInfo.bluetoothDevice == selectedDevice?.bluetoothDevice) + MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.surface ) ) { - Row(modifier=Modifier.padding(8.dp)) { + Row(modifier = Modifier.padding(8.dp)) { Column(verticalArrangement = Arrangement.Center) { - Text(text=deviceInfo.bluetoothDevice.name, fontWeight = FontWeight.Bold, softWrap = true) - Text(text="MAC: ${deviceInfo.bluetoothDevice.address}", fontWeight = FontWeight.Thin) - } - Box(contentAlignment = Alignment.CenterEnd, modifier = Modifier.fillMaxSize()) { - Icon( - painterResource(id = getSignalIconForRssiValue(deviceInfo.rssi)), - contentDescription = null, - modifier = Modifier - .fillMaxHeight() - .padding(10.dp), - tint = MaterialTheme.colorScheme.primary + Text( + text = deviceInfo.bluetoothDevice.name, + fontWeight = FontWeight.Bold, + softWrap = true ) + Text( + text = "MAC: ${deviceInfo.bluetoothDevice.address}", + fontWeight = FontWeight.Thin + ) + } + if (!deviceInfo.isPaired) { + Box(contentAlignment = Alignment.CenterEnd, modifier = Modifier.fillMaxSize()) { + Icon( + painterResource(id = getSignalIconForRssiValue(deviceInfo.rssi)), + contentDescription = null, + modifier = Modifier + .fillMaxHeight() + .padding(10.dp), + tint = MaterialTheme.colorScheme.primary + ) + } } } } } -fun getSignalIconForRssiValue(rssi: Short): Int{ +fun getSignalIconForRssiValue(rssi: Short): Int { if (rssi >= -80) return R.drawable.signal_icon4 else if (rssi >= -90) return R.drawable.signal_icon3 else if (rssi >= -100) return R.drawable.signal_icon2 diff --git a/app/src/main/java/com/helible/pilot/components/DiscoveredDevicesList.kt b/app/src/main/java/com/helible/pilot/components/DiscoveredDevicesList.kt index e86e26b..0a7442c 100644 --- a/app/src/main/java/com/helible/pilot/components/DiscoveredDevicesList.kt +++ b/app/src/main/java/com/helible/pilot/components/DiscoveredDevicesList.kt @@ -1,18 +1,36 @@ package com.helible.pilot.components +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import com.helible.pilot.BluetoothUiState import com.helible.pilot.Device @Composable -fun DiscoveredDevicesList(devices: List, selectedDevice: Device?, choiceDevice: (device: Device?) -> Unit, modifier: Modifier = Modifier) { +fun DiscoveredDevicesList( + bluetoothState: BluetoothUiState, + selectedDevice: Device?, + choiceDevice: (device: Device?) -> Unit, + modifier: Modifier = Modifier +) { LazyColumn(modifier = modifier) { - items(devices) { device -> + item { + Text( + text = "Ранее подключенные устройства", + textAlign = TextAlign.Left, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(10.dp) + ) + } + items(bluetoothState.pairedDevices) { device -> DeviceItem( deviceInfo = device, selectedDevice = selectedDevice, @@ -24,5 +42,44 @@ fun DiscoveredDevicesList(devices: List, selectedDevice: Device?, choice ) ) } + if(bluetoothState.pairedDevices.isEmpty()){ + item { + Text( + text = "Нет элементов для отображения", + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxSize() + ) + } + } + + item { + Text( + text = "Доступные устройства", + textAlign = TextAlign.Left, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(10.dp) + ) + } + + items(bluetoothState.scannedDevices) { device -> + DeviceItem( + deviceInfo = device, + selectedDevice = selectedDevice, + choiceDevice = choiceDevice, + modifier = Modifier + .fillMaxWidth() + .padding( + 5.dp + ) + ) + } + if(bluetoothState.pairedDevices.isEmpty() && !bluetoothState.isDiscovering) { + item { + Text( + text = "Устройства поблизости не обнаружены.", + modifier = Modifier.fillMaxSize() + ) + } + } } } \ No newline at end of file