diff --git a/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/L2capClient.kt b/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/L2capClient.kt index 7722bb8..874bc26 100644 --- a/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/L2capClient.kt +++ b/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/L2capClient.kt @@ -16,17 +16,63 @@ package com.github.google.bumble.btbench import android.annotation.SuppressLint import android.bluetooth.BluetoothAdapter -import java.io.IOException +import android.bluetooth.BluetoothDevice +import android.bluetooth.BluetoothGatt +import android.bluetooth.BluetoothGattCallback +import android.bluetooth.BluetoothProfile +import android.content.Context import java.util.logging.Logger -import kotlin.concurrent.thread private val Log = Logger.getLogger("btbench.l2cap-client") -class L2capClient(private val viewModel: AppViewModel, val bluetoothAdapter: BluetoothAdapter) { +class L2capClient( + private val viewModel: AppViewModel, + val bluetoothAdapter: BluetoothAdapter, + val context: Context +) { @SuppressLint("MissingPermission") fun run() { viewModel.running = true val remoteDevice = bluetoothAdapter.getRemoteDevice(viewModel.peerBluetoothAddress) + + val gatt = remoteDevice.connectGatt( + context, + false, + object : BluetoothGattCallback() { + override fun onMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int) { + Log.info("MTU update: mtu=$mtu status=$status") + viewModel.mtu = mtu + } + + override fun onPhyUpdate(gatt: BluetoothGatt, txPhy: Int, rxPhy: Int, status: Int) { + Log.info("PHY update: tx=$txPhy, rx=$rxPhy, status=$status") + viewModel.txPhy = txPhy + viewModel.rxPhy = rxPhy + } + + override fun onPhyRead(gatt: BluetoothGatt, txPhy: Int, rxPhy: Int, status: Int) { + Log.info("PHY: tx=$txPhy, rx=$rxPhy, status=$status") + viewModel.txPhy = txPhy + viewModel.rxPhy = rxPhy + } + + override fun onConnectionStateChange( + gatt: BluetoothGatt?, status: Int, newState: Int + ) { + if (gatt != null && newState == BluetoothProfile.STATE_CONNECTED) { + gatt.setPreferredPhy( + BluetoothDevice.PHY_LE_2M_MASK, + BluetoothDevice.PHY_LE_2M_MASK, + BluetoothDevice.PHY_OPTION_NO_PREFERRED + ) + gatt.readPhy() + } + } + }, + BluetoothDevice.TRANSPORT_LE, + if (viewModel.use2mPhy) BluetoothDevice.PHY_LE_2M_MASK else BluetoothDevice.PHY_LE_1M_MASK + ) + val socket = remoteDevice.createInsecureL2capChannel(viewModel.l2capPsm) val client = SocketClient(viewModel, socket) diff --git a/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/L2capServer.kt b/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/L2capServer.kt index 79c7004..76c297b 100644 --- a/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/L2capServer.kt +++ b/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/L2capServer.kt @@ -30,7 +30,7 @@ private val Log = Logger.getLogger("btbench.l2cap-server") class L2capServer(private val viewModel: AppViewModel, private val bluetoothAdapter: BluetoothAdapter) { @SuppressLint("MissingPermission") fun run() { - // Advertise to that the peer can find us and connect. + // Advertise so that the peer can find us and connect. val callback = object: AdvertiseCallback() { override fun onStartFailure(errorCode: Int) { Log.warning("failed to start advertising: $errorCode") @@ -50,13 +50,12 @@ class L2capServer(private val viewModel: AppViewModel, private val bluetoothAdap val advertiseData = AdvertiseData.Builder().build() val scanData = AdvertiseData.Builder().setIncludeDeviceName(true).build() val advertiser = bluetoothAdapter.bluetoothLeAdvertiser - advertiser.startAdvertising(advertiseSettings, advertiseData, scanData, callback) val serverSocket = bluetoothAdapter.listenUsingInsecureL2capChannel() viewModel.l2capPsm = serverSocket.psm Log.info("psm = $serverSocket.psm") val server = SocketServer(viewModel, serverSocket) - server.run({ advertiser.stopAdvertising(callback) }) + server.run({ advertiser.stopAdvertising(callback) }, { advertiser.startAdvertising(advertiseSettings, advertiseData, scanData, callback) }) } } \ No newline at end of file diff --git a/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/MainActivity.kt b/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/MainActivity.kt index 314f746..6081837 100644 --- a/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/MainActivity.kt +++ b/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/MainActivity.kt @@ -26,23 +26,33 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column 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.padding +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Button import androidx.compose.material3.Divider import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Slider import androidx.compose.material3.Surface +import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.TextField import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.ImeAction @@ -171,7 +181,7 @@ class MainActivity : ComponentActivity() { } private fun runL2capClient() { - val l2capClient = bluetoothAdapter?.let { L2capClient(appViewModel, it) } + val l2capClient = bluetoothAdapter?.let { L2capClient(appViewModel, it, baseContext) } l2capClient?.run() } @@ -199,9 +209,12 @@ fun MainView( runL2capServer: () -> Unit ) { BTBenchTheme { - // A surface container using the 'background' color from the theme + val scrollState = rememberScrollState() Surface( - modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background + modifier = Modifier + .fillMaxSize() + .verticalScroll(scrollState), + color = MaterialTheme.colorScheme.background ) { Column(modifier = Modifier.padding(horizontal = 16.dp)) { Text( @@ -212,28 +225,33 @@ fun MainView( ) Divider() val keyboardController = LocalSoftwareKeyboardController.current - TextField(label = { - Text(text = "Peer Bluetooth Address") - }, + val focusRequester = remember { FocusRequester() } + val focusManager = LocalFocusManager.current + TextField( + label = { + Text(text = "Peer Bluetooth Address") + }, value = appViewModel.peerBluetoothAddress, - modifier = Modifier.fillMaxWidth(), + modifier = Modifier.fillMaxWidth().focusRequester(focusRequester), keyboardOptions = KeyboardOptions.Default.copy( keyboardType = KeyboardType.Ascii, imeAction = ImeAction.Done ), onValueChange = { appViewModel.updatePeerBluetoothAddress(it) }, - keyboardActions = KeyboardActions(onDone = { keyboardController?.hide() }) + keyboardActions = KeyboardActions(onDone = { + keyboardController?.hide() + focusManager.clearFocus() + }) ) Divider() TextField(label = { Text(text = "L2CAP PSM") }, value = appViewModel.l2capPsm.toString(), - modifier = Modifier.fillMaxWidth(), + modifier = Modifier.fillMaxWidth().focusRequester(focusRequester), keyboardOptions = KeyboardOptions.Default.copy( - keyboardType = KeyboardType.Number, - imeAction = ImeAction.Done + keyboardType = KeyboardType.Number, imeAction = ImeAction.Done ), onValueChange = { if (it.isNotEmpty()) { @@ -243,7 +261,11 @@ fun MainView( } } }, - keyboardActions = KeyboardActions(onDone = { keyboardController?.hide() })) + keyboardActions = KeyboardActions(onDone = { + keyboardController?.hide() + focusManager.clearFocus() + }) + ) Divider() Slider( value = appViewModel.senderPacketCountSlider, onValueChange = { @@ -264,7 +286,19 @@ fun MainView( ActionButton( text = "Become Discoverable", onClick = becomeDiscoverable, true ) - Row() { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text(text = "2M PHY") + Spacer(modifier = Modifier.padding(start = 8.dp)) + Switch( + checked = appViewModel.use2mPhy, + onCheckedChange = { appViewModel.use2mPhy = it } + ) + + } + Row { ActionButton( text = "RFCOMM Client", onClick = runRfcommClient, !appViewModel.running ) @@ -272,7 +306,7 @@ fun MainView( text = "RFCOMM Server", onClick = runRfcommServer, !appViewModel.running ) } - Row() { + Row { ActionButton( text = "L2CAP Client", onClick = runL2capClient, !appViewModel.running ) @@ -281,6 +315,12 @@ fun MainView( ) } Divider() + Text( + text = if (appViewModel.mtu != 0) "MTU: ${appViewModel.mtu}" else "" + ) + Text( + text = if (appViewModel.rxPhy != 0 || appViewModel.txPhy != 0) "PHY: tx=${appViewModel.txPhy}, rx=${appViewModel.rxPhy}" else "" + ) Text( text = "Packets Sent: ${appViewModel.packetsSent}" ) diff --git a/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/Model.kt b/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/Model.kt index 93755e4..b709be3 100644 --- a/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/Model.kt +++ b/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/Model.kt @@ -32,6 +32,10 @@ class AppViewModel : ViewModel() { private var preferences: SharedPreferences? = null var peerBluetoothAddress by mutableStateOf(DEFAULT_PEER_BLUETOOTH_ADDRESS) var l2capPsm by mutableStateOf(0) + var use2mPhy by mutableStateOf(true) + var mtu by mutableStateOf(0) + var rxPhy by mutableStateOf(0) + var txPhy by mutableStateOf(0) var senderPacketCountSlider by mutableFloatStateOf(0.0F) var senderPacketSizeSlider by mutableFloatStateOf(0.0F) var senderPacketCount by mutableIntStateOf(DEFAULT_SENDER_PACKET_COUNT) @@ -116,7 +120,7 @@ class AppViewModel : ViewModel() { } fun updateSenderPacketSizeSlider() { - if (senderPacketSize <= 1) { + if (senderPacketSize <= 16) { senderPacketSizeSlider = 0.0F } else if (senderPacketSize <= 256) { senderPacketSizeSlider = 0.02F @@ -138,7 +142,7 @@ class AppViewModel : ViewModel() { fun updateSenderPacketSize() { if (senderPacketSizeSlider < 0.1F) { - senderPacketSize = 1 + senderPacketSize = 16 } else if (senderPacketSizeSlider < 0.3F) { senderPacketSize = 256 } else if (senderPacketSizeSlider < 0.5F) { diff --git a/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/RfcommServer.kt b/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/RfcommServer.kt index f06736b..69612c5 100644 --- a/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/RfcommServer.kt +++ b/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/RfcommServer.kt @@ -30,6 +30,6 @@ class RfcommServer(private val viewModel: AppViewModel, val bluetoothAdapter: Bl ) val server = SocketServer(viewModel, serverSocket) - server.run({}) + server.run({}, {}) } } \ No newline at end of file diff --git a/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/SocketClient.kt b/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/SocketClient.kt index cc5058e..28c5354 100644 --- a/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/SocketClient.kt +++ b/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/SocketClient.kt @@ -22,6 +22,8 @@ import kotlin.concurrent.thread private val Log = Logger.getLogger("btbench.socket-client") +private const val DEFAULT_STARTUP_DELAY = 1000 + class SocketClient(private val viewModel: AppViewModel, private val socket: BluetoothSocket) { @SuppressLint("MissingPermission") fun run() { @@ -56,6 +58,10 @@ class SocketClient(private val viewModel: AppViewModel, private val socket: Blue socketDataSource.receive() } + Log.info("Startup delay: $DEFAULT_STARTUP_DELAY") + Thread.sleep(DEFAULT_STARTUP_DELAY.toLong()); + Log.info("Starting to send") + sender.run() cleanup() } diff --git a/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/SocketServer.kt b/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/SocketServer.kt index 3f9c3e1..e461617 100644 --- a/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/SocketServer.kt +++ b/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/SocketServer.kt @@ -22,14 +22,13 @@ import kotlin.concurrent.thread private val Log = Logger.getLogger("btbench.socket-server") class SocketServer(private val viewModel: AppViewModel, private val serverSocket: BluetoothServerSocket) { - fun run(onTerminate: () -> Unit) { + fun run(onConnected: () -> Unit, onDisconnected: () -> Unit) { var aborted = false viewModel.running = true fun cleanup() { serverSocket.close() viewModel.running = false - onTerminate() } thread(name = "SocketServer") { @@ -38,6 +37,7 @@ class SocketServer(private val viewModel: AppViewModel, private val serverSocket serverSocket.close() } Log.info("waiting for connection...") + onDisconnected() val socket = try { serverSocket.accept() } catch (error: IOException) { @@ -46,6 +46,7 @@ class SocketServer(private val viewModel: AppViewModel, private val serverSocket return@thread } Log.info("got connection") + onConnected() viewModel.aborter = { aborted = true diff --git a/extras/android/RemoteHCI/app/src/main/java/com/github/google/bumble/remotehci/MainActivity.kt b/extras/android/RemoteHCI/app/src/main/java/com/github/google/bumble/remotehci/MainActivity.kt index ebdc708..493b7e5 100644 --- a/extras/android/RemoteHCI/app/src/main/java/com/github/google/bumble/remotehci/MainActivity.kt +++ b/extras/android/RemoteHCI/app/src/main/java/com/github/google/bumble/remotehci/MainActivity.kt @@ -10,8 +10,10 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Button import androidx.compose.material3.Divider import androidx.compose.material3.ExperimentalMaterial3Api @@ -71,7 +73,7 @@ class AppViewModel : ViewModel(), HciProxy.Listener { this.tcpPort = tcpPort // Save the port to the preferences - with (preferences!!.edit()) { + with(preferences!!.edit()) { putString(TCP_PORT_PREF_KEY, tcpPort.toString()) apply() } @@ -138,7 +140,8 @@ class MainActivity : ComponentActivity() { log.warning("Exception while running HCI Server: $error") } catch (error: HalException) { log.warning("HAL exception: ${error.message}") - appViewModel.message = "Cannot bind to HAL (${error.message}). You may need to use the command 'setenforce 0' in a root adb shell." + appViewModel.message = + "Cannot bind to HAL (${error.message}). You may need to use the command 'setenforce 0' in a root adb shell." } log.info("HCI Proxy thread ended") appViewModel.canStart = true @@ -157,9 +160,12 @@ fun ActionButton(text: String, onClick: () -> Unit, enabled: Boolean) { @Composable fun MainView(appViewModel: AppViewModel, startProxy: () -> Unit) { RemoteHCITheme { - // A surface container using the 'background' color from the theme + val scrollState = rememberScrollState() Surface( - modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background + modifier = Modifier + .fillMaxSize() + .verticalScroll(scrollState), + color = MaterialTheme.colorScheme.background ) { Column(modifier = Modifier.padding(horizontal = 16.dp)) { Text( @@ -174,13 +180,15 @@ fun MainView(appViewModel: AppViewModel, startProxy: () -> Unit) { ) Divider() val keyboardController = LocalSoftwareKeyboardController.current - TextField( - label = { - Text(text = "TCP Port") - }, + TextField(label = { + Text(text = "TCP Port") + }, value = appViewModel.tcpPort.toString(), modifier = Modifier.fillMaxWidth(), - keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number, imeAction = ImeAction.Done), + keyboardOptions = KeyboardOptions.Default.copy( + keyboardType = KeyboardType.Number, + imeAction = ImeAction.Done + ), onValueChange = { if (it.isNotEmpty()) { val tcpPort = it.toIntOrNull() @@ -189,10 +197,7 @@ fun MainView(appViewModel: AppViewModel, startProxy: () -> Unit) { } } }, - keyboardActions = KeyboardActions( - onDone = {keyboardController?.hide()} - ) - ) + keyboardActions = KeyboardActions(onDone = { keyboardController?.hide() })) Divider() val connectState = if (appViewModel.hostConnected) "CONNECTED" else "DISCONNECTED" Text(