`ConnectionsScreen` available BLE devices (#3298)

pull/3300/head
Phil Oliver 2025-10-02 20:18:09 -04:00 zatwierdzone przez GitHub
rodzic 526ca9b854
commit 502e417338
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: B5690EEEBB952194
3 zmienionych plików z 48 dodań i 83 usunięć

Wyświetl plik

@ -25,23 +25,18 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.selection.selectableGroup
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Language
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@ -49,14 +44,12 @@ import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ConfigProtos
@ -118,7 +111,8 @@ fun ConnectionsScreen(
val regionUnset = config.lora.region == ConfigProtos.Config.LoRaConfig.RegionCode.UNSET
val bluetoothRssi by connectionsViewModel.bluetoothRssi.collectAsStateWithLifecycle()
val bleDevices by scanModel.bleDevicesForUi.collectAsStateWithLifecycle()
val bondedBleDevices by scanModel.bleDevicesForUi.collectAsStateWithLifecycle()
val scannedBleDevices by scanModel.scanResult.observeAsState(emptyMap())
val discoveredTcpDevices by scanModel.discoveredTcpDevicesForUi.collectAsStateWithLifecycle()
val recentTcpDevices by scanModel.recentTcpDevicesForUi.collectAsStateWithLifecycle()
val usbDevices by scanModel.usbDevicesForUi.collectAsStateWithLifecycle()
@ -152,15 +146,6 @@ fun ConnectionsScreen(
}
}
// State for the device scan dialog
var showScanDialog by remember { mutableStateOf(false) }
val scanResults by scanModel.scanResult.observeAsState(emptyMap())
// Observe scan results to show the dialog
if (scanResults.isNotEmpty()) {
showScanDialog = true
}
LaunchedEffect(connectionState, regionUnset) {
when (connectionState) {
ConnectionState.CONNECTED -> {
@ -245,7 +230,11 @@ fun ConnectionsScreen(
DeviceType.BLE -> {
BLEDevices(
connectionState = connectionState,
btDevices = bleDevices,
bondedDevices = bondedBleDevices,
availableDevices =
scannedBleDevices.values.toList().filterNot { available ->
bondedBleDevices.any { it.address == available.address }
},
selectedDevice = selectedDevice,
scanModel = scanModel,
bluetoothEnabled = bluetoothState.enabled,
@ -280,7 +269,7 @@ fun ConnectionsScreen(
val showWarningNotPaired =
!connectionState.isConnected() &&
!hasShownNotPairedWarning &&
bleDevices.none { it is DeviceListEntry.Ble && it.bonded }
bondedBleDevices.none { it is DeviceListEntry.Ble && it.bonded }
if (showWarningNotPaired) {
Text(
text = stringResource(R.string.warning_not_paired),
@ -294,55 +283,6 @@ fun ConnectionsScreen(
}
}
}
// Compose Device Scan Dialog
if (showScanDialog) {
Dialog(
onDismissRequest = {
showScanDialog = false
scanModel.clearScanResults()
},
) {
Surface(shape = MaterialTheme.shapes.medium) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "Select a Bluetooth device",
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(bottom = 16.dp),
)
Column(modifier = Modifier.selectableGroup()) {
scanResults.values.forEach { device ->
Row(
modifier =
Modifier.fillMaxWidth()
.selectable(
selected = false, // No pre-selection in this dialog
onClick = {
scanModel.onSelected(device)
scanModel.clearScanResults()
showScanDialog = false
},
)
.padding(vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(text = device.name)
}
}
}
Spacer(modifier = Modifier.height(16.dp))
TextButton(
onClick = {
scanModel.clearScanResults()
showScanDialog = false
},
) {
Text(stringResource(R.string.cancel))
}
}
}
}
}
}
Box(modifier = Modifier.fillMaxWidth().padding(8.dp)) {

Wyświetl plik

@ -69,7 +69,8 @@ import org.meshtastic.core.ui.component.TitledCard
@Composable
fun BLEDevices(
connectionState: ConnectionState,
btDevices: List<DeviceListEntry>,
bondedDevices: List<DeviceListEntry>,
availableDevices: List<DeviceListEntry>,
selectedDevice: String,
scanModel: BTScanModel,
bluetoothEnabled: Boolean,
@ -153,7 +154,7 @@ fun BLEDevices(
}
}
if (btDevices.isEmpty()) {
if (bondedDevices.isEmpty() && availableDevices.isEmpty()) {
EmptyStateContent(
imageVector = Icons.Rounded.BluetoothDisabled,
text =
@ -165,18 +166,19 @@ fun BLEDevices(
actionButton = scanButton,
)
} else {
TitledCard(title = stringResource(R.string.bluetooth_paired_devices)) {
btDevices.forEach { device ->
val connected =
connectionState == ConnectionState.CONNECTED && device.fullAddress == selectedDevice
DeviceListItem(
connected = connected,
device = device,
onSelect = { scanModel.onSelected(device) },
modifier = Modifier,
)
}
}
bondedDevices.Section(
title = stringResource(R.string.bluetooth_paired_devices),
connectionState = connectionState,
selectedDevice = selectedDevice,
onSelect = scanModel::onSelected,
)
availableDevices.Section(
title = stringResource(R.string.bluetooth_available_devices),
connectionState = connectionState,
selectedDevice = selectedDevice,
onSelect = scanModel::onSelected,
)
scanButton()
}
@ -213,3 +215,25 @@ private fun checkPermissionsAndScan(
permissionsState.launchMultiplePermissionRequest()
}
}
@Composable
private fun List<DeviceListEntry>.Section(
title: String,
connectionState: ConnectionState,
selectedDevice: String,
onSelect: (DeviceListEntry) -> Unit,
) {
if (isNotEmpty()) {
TitledCard(title = title) {
forEach { device ->
val connected = connectionState == ConnectionState.CONNECTED && device.fullAddress == selectedDevice
DeviceListItem(
connected = connected,
device = device,
onSelect = { onSelect(device) },
modifier = Modifier,
)
}
}
}
}

Wyświetl plik

@ -821,7 +821,8 @@
<string name="no_pax_metrics_logs">No PAX metrics logs available.</string>
<string name="wifi_devices">WiFi Devices</string>
<string name="ble_devices">BLE Devices</string>
<string name="bluetooth_paired_devices">Paired Devices</string>
<string name="bluetooth_paired_devices">Paired devices</string>
<string name="bluetooth_available_devices">Available devices</string>
<string name="connected_device">Connected Device</string>
<string name="routing_error_rate_limit_exceeded">Rate Limit Exceeded. Please try again later.</string>