diff --git a/apps/bench.py b/apps/bench.py
index 8b37883..de14eee 100644
--- a/apps/bench.py
+++ b/apps/bench.py
@@ -77,6 +77,7 @@ SPEED_SERVICE_UUID = '50DB505C-8AC4-4738-8448-3B1D9CC09CC5'
SPEED_TX_UUID = 'E789C754-41A1-45F4-A948-A0A1A90DBA53'
SPEED_RX_UUID = '016A2CC7-E14B-4819-935F-1F56EAE4098D'
+DEFAULT_RFCOMM_UUID = 'E6D55659-C8B4-4B85-96BB-B1143AF6D3AE'
DEFAULT_L2CAP_PSM = 1234
DEFAULT_L2CAP_MAX_CREDITS = 128
DEFAULT_L2CAP_MTU = 1022
@@ -128,11 +129,16 @@ def print_connection(connection):
if connection.transport == BT_LE_TRANSPORT:
phy_state = (
'PHY='
- f'RX:{le_phy_name(connection.phy.rx_phy)}/'
- f'TX:{le_phy_name(connection.phy.tx_phy)}'
+ f'TX:{le_phy_name(connection.phy.tx_phy)}/'
+ f'RX:{le_phy_name(connection.phy.rx_phy)}'
)
- data_length = f'DL={connection.data_length}'
+ data_length = (
+ 'DL=('
+ f'TX:{connection.data_length[0]}/{connection.data_length[1]},'
+ f'RX:{connection.data_length[2]}/{connection.data_length[3]}'
+ ')'
+ )
connection_parameters = (
'Parameters='
f'{connection.parameters.connection_interval * 1.25:.2f}/'
@@ -169,9 +175,7 @@ def make_sdp_records(channel):
),
ServiceAttribute(
SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID,
- DataElement.sequence(
- [DataElement.uuid(UUID('E6D55659-C8B4-4B85-96BB-B1143AF6D3AE'))]
- ),
+ DataElement.sequence([DataElement.uuid(UUID(DEFAULT_RFCOMM_UUID))]),
),
ServiceAttribute(
SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
@@ -224,7 +228,7 @@ class Sender:
if self.tx_start_delay:
print(color(f'*** Startup delay: {self.tx_start_delay}', 'blue'))
- await asyncio.sleep(self.tx_start_delay) # FIXME
+ await asyncio.sleep(self.tx_start_delay)
print(color('=== Sending RESET', 'magenta'))
await self.packet_io.send_packet(bytes([PacketType.RESET]))
@@ -364,7 +368,7 @@ class Ping:
if self.tx_start_delay:
print(color(f'*** Startup delay: {self.tx_start_delay}', 'blue'))
- await asyncio.sleep(self.tx_start_delay) # FIXME
+ await asyncio.sleep(self.tx_start_delay)
print(color('=== Sending RESET', 'magenta'))
await self.packet_io.send_packet(bytes([PacketType.RESET]))
@@ -710,14 +714,14 @@ class L2capServer(StreamedPacketIO):
self.l2cap_channel = None
self.ready = asyncio.Event()
- # Listen for incoming L2CAP CoC connections
+ # Listen for incoming L2CAP connections
device.create_l2cap_server(
spec=l2cap.LeCreditBasedChannelSpec(
psm=psm, mtu=mtu, mps=mps, max_credits=max_credits
),
handler=self.on_l2cap_channel,
)
- print(color(f'### Listening for CoC connection on PSM {psm}', 'yellow'))
+ print(color(f'### Listening for L2CAP connection on PSM {psm}', 'yellow'))
async def on_connection(self, connection):
connection.on('disconnection', self.on_disconnection)
@@ -743,9 +747,10 @@ class L2capServer(StreamedPacketIO):
# RfcommClient
# -----------------------------------------------------------------------------
class RfcommClient(StreamedPacketIO):
- def __init__(self, device):
+ def __init__(self, device, channel):
super().__init__()
self.device = device
+ self.channel = channel
self.ready = asyncio.Event()
async def on_connection(self, connection):
@@ -757,10 +762,9 @@ class RfcommClient(StreamedPacketIO):
rfcomm_mux = await rfcomm_client.start()
print(color('*** Started', 'blue'))
- channel = DEFAULT_RFCOMM_CHANNEL
- print(color(f'### Opening session for channel {channel}...', 'yellow'))
+ print(color(f'### Opening session for channel {self.channel}...', 'yellow'))
try:
- rfcomm_session = await rfcomm_mux.open_dlc(channel)
+ rfcomm_session = await rfcomm_mux.open_dlc(self.channel)
print(color('### Session open', 'yellow'), rfcomm_session)
except bumble.core.ConnectionError as error:
print(color(f'!!! Session open failed: {error}', 'red'))
@@ -780,7 +784,7 @@ class RfcommClient(StreamedPacketIO):
# RfcommServer
# -----------------------------------------------------------------------------
class RfcommServer(StreamedPacketIO):
- def __init__(self, device):
+ def __init__(self, device, channel):
super().__init__()
self.ready = asyncio.Event()
@@ -788,7 +792,7 @@ class RfcommServer(StreamedPacketIO):
rfcomm_server = bumble.rfcomm.Server(device)
# Listen for incoming DLC connections
- channel_number = rfcomm_server.listen(self.on_dlc, DEFAULT_RFCOMM_CHANNEL)
+ channel_number = rfcomm_server.listen(self.on_dlc, channel)
# Setup the SDP to advertise this channel
device.sdp_service_records = make_sdp_records(channel_number)
@@ -825,6 +829,9 @@ class Central(Connection.Listener):
mode_factory,
connection_interval,
phy,
+ authenticate,
+ encrypt,
+ extended_data_length,
):
super().__init__()
self.transport = transport
@@ -832,6 +839,9 @@ class Central(Connection.Listener):
self.classic = classic
self.role_factory = role_factory
self.mode_factory = mode_factory
+ self.authenticate = authenticate
+ self.encrypt = encrypt or authenticate
+ self.extended_data_length = extended_data_length
self.device = None
self.connection = None
@@ -904,7 +914,26 @@ class Central(Connection.Listener):
self.connection.listener = self
print_connection(self.connection)
- await mode.on_connection(self.connection)
+ # Request a new data length if requested
+ if self.extended_data_length:
+ print(color('+++ Requesting extended data length', 'cyan'))
+ await self.connection.set_data_length(
+ self.extended_data_length[0], self.extended_data_length[1]
+ )
+
+ # Authenticate if requested
+ if self.authenticate:
+ # Request authentication
+ print(color('*** Authenticating...', 'cyan'))
+ await self.connection.authenticate()
+ print(color('*** Authenticated', 'cyan'))
+
+ # Encrypt if requested
+ if self.encrypt:
+ # Enable encryption
+ print(color('*** Enabling encryption...', 'cyan'))
+ await self.connection.encrypt()
+ print(color('*** Encryption on', 'cyan'))
# Set the PHY if requested
if self.phy is not None:
@@ -919,6 +948,8 @@ class Central(Connection.Listener):
)
)
+ await mode.on_connection(self.connection)
+
await role.run()
await asyncio.sleep(DEFAULT_LINGER_TIME)
@@ -943,9 +974,12 @@ class Central(Connection.Listener):
# Peripheral
# -----------------------------------------------------------------------------
class Peripheral(Device.Listener, Connection.Listener):
- def __init__(self, transport, classic, role_factory, mode_factory):
+ def __init__(
+ self, transport, classic, extended_data_length, role_factory, mode_factory
+ ):
self.transport = transport
self.classic = classic
+ self.extended_data_length = extended_data_length
self.role_factory = role_factory
self.role = None
self.mode_factory = mode_factory
@@ -1006,6 +1040,15 @@ class Peripheral(Device.Listener, Connection.Listener):
self.connection = connection
self.connected.set()
+ # Request a new data length if needed
+ if self.extended_data_length:
+ print("+++ Requesting extended data length")
+ AsyncRunner.spawn(
+ connection.set_data_length(
+ self.extended_data_length[0], self.extended_data_length[1]
+ )
+ )
+
def on_disconnection(self, reason):
print(color(f'!!! Disconnection: reason={reason}', 'red'))
self.connection = None
@@ -1038,16 +1081,16 @@ def create_mode_factory(ctx, default_mode):
return GattServer(device)
if mode == 'l2cap-client':
- return L2capClient(device)
+ return L2capClient(device, psm=ctx.obj['l2cap_psm'])
if mode == 'l2cap-server':
- return L2capServer(device)
+ return L2capServer(device, psm=ctx.obj['l2cap_psm'])
if mode == 'rfcomm-client':
- return RfcommClient(device)
+ return RfcommClient(device, channel=ctx.obj['rfcomm_channel'])
if mode == 'rfcomm-server':
- return RfcommServer(device)
+ return RfcommServer(device, channel=ctx.obj['rfcomm_channel'])
raise ValueError('invalid mode')
@@ -1113,6 +1156,22 @@ def create_role_factory(ctx, default_role):
type=click.IntRange(23, 517),
help='GATT MTU (gatt-client mode)',
)
+@click.option(
+ '--extended-data-length',
+ help='Request a data length upon connection, specified as tx_octets/tx_time',
+)
+@click.option(
+ '--rfcomm-channel',
+ type=int,
+ default=DEFAULT_RFCOMM_CHANNEL,
+ help='RFComm channel to use',
+)
+@click.option(
+ '--l2cap-psm',
+ type=int,
+ default=DEFAULT_L2CAP_PSM,
+ help='L2CAP PSM to use',
+)
@click.option(
'--packet-size',
'-s',
@@ -1139,17 +1198,34 @@ def create_role_factory(ctx, default_role):
)
@click.pass_context
def bench(
- ctx, device_config, role, mode, att_mtu, packet_size, packet_count, start_delay
+ ctx,
+ device_config,
+ role,
+ mode,
+ att_mtu,
+ extended_data_length,
+ packet_size,
+ packet_count,
+ start_delay,
+ rfcomm_channel,
+ l2cap_psm,
):
ctx.ensure_object(dict)
ctx.obj['device_config'] = device_config
ctx.obj['role'] = role
ctx.obj['mode'] = mode
ctx.obj['att_mtu'] = att_mtu
+ ctx.obj['rfcomm_channel'] = rfcomm_channel
+ ctx.obj['l2cap_psm'] = l2cap_psm
ctx.obj['packet_size'] = packet_size
ctx.obj['packet_count'] = packet_count
ctx.obj['start_delay'] = start_delay
+ ctx.obj['extended_data_length'] = (
+ [int(x) for x in extended_data_length.split('/')]
+ if extended_data_length
+ else None
+ )
ctx.obj['classic'] = mode in ('rfcomm-client', 'rfcomm-server')
@@ -1170,8 +1246,12 @@ def bench(
help='Connection interval (in ms)',
)
@click.option('--phy', type=click.Choice(['1m', '2m', 'coded']), help='PHY to use')
+@click.option('--authenticate', is_flag=True, help='Authenticate (RFComm only)')
+@click.option('--encrypt', is_flag=True, help='Encrypt the connection (RFComm only)')
@click.pass_context
-def central(ctx, transport, peripheral_address, connection_interval, phy):
+def central(
+ ctx, transport, peripheral_address, connection_interval, phy, authenticate, encrypt
+):
"""Run as a central (initiates the connection)"""
role_factory = create_role_factory(ctx, 'sender')
mode_factory = create_mode_factory(ctx, 'gatt-client')
@@ -1186,6 +1266,9 @@ def central(ctx, transport, peripheral_address, connection_interval, phy):
mode_factory,
connection_interval,
phy,
+ authenticate,
+ encrypt or authenticate,
+ ctx.obj['extended_data_length'],
).run()
)
@@ -1199,7 +1282,13 @@ def peripheral(ctx, transport):
mode_factory = create_mode_factory(ctx, 'gatt-server')
asyncio.run(
- Peripheral(transport, ctx.obj['classic'], role_factory, mode_factory).run()
+ Peripheral(
+ transport,
+ ctx.obj['classic'],
+ ctx.obj['extended_data_length'],
+ role_factory,
+ mode_factory,
+ ).run()
)
diff --git a/apps/controller_info.py b/apps/controller_info.py
index 5be4f3d..1e02a32 100644
--- a/apps/controller_info.py
+++ b/apps/controller_info.py
@@ -42,6 +42,8 @@ from bumble.hci import (
HCI_LE_Read_Number_Of_Supported_Advertising_Sets_Command,
HCI_LE_READ_MAXIMUM_ADVERTISING_DATA_LENGTH_COMMAND,
HCI_LE_Read_Maximum_Advertising_Data_Length_Command,
+ HCI_LE_READ_SUGGESTED_DEFAULT_DATA_LENGTH_COMMAND,
+ HCI_LE_Read_Suggested_Default_Data_Length_Command,
)
from bumble.host import Host
from bumble.transport import open_transport_or_link
@@ -117,6 +119,18 @@ async def get_le_info(host):
'\n',
)
+ if host.supports_command(HCI_LE_READ_SUGGESTED_DEFAULT_DATA_LENGTH_COMMAND):
+ response = await host.send_command(
+ HCI_LE_Read_Suggested_Default_Data_Length_Command()
+ )
+ if command_succeeded(response):
+ print(
+ color('Suggested Default Data Length:', 'yellow'),
+ f'{response.return_parameters.suggested_max_tx_octets}/'
+ f'{response.return_parameters.suggested_max_tx_time}',
+ '\n',
+ )
+
print(color('LE Features:', 'yellow'))
for feature in host.supported_le_features:
print(' ', name_or_number(HCI_LE_SUPPORTED_FEATURES_NAMES, feature))
diff --git a/apps/pair.py b/apps/pair.py
index 996b01c..884882a 100644
--- a/apps/pair.py
+++ b/apps/pair.py
@@ -28,12 +28,16 @@ from bumble.pairing import OobData, PairingDelegate, PairingConfig
from bumble.smp import OobContext, OobLegacyContext
from bumble.smp import error_name as smp_error_name
from bumble.keys import JsonKeyStore
+<<<<<<< HEAD
from bumble.core import (
AdvertisingData,
ProtocolError,
BT_LE_TRANSPORT,
BT_BR_EDR_TRANSPORT,
)
+=======
+from bumble.core import ProtocolError, BT_LE_TRANSPORT, BT_BR_EDR_TRANSPORT
+>>>>>>> e3de14f (first implementation (+1 squashed commit))
from bumble.gatt import (
GATT_DEVICE_NAME_CHARACTERISTIC,
GATT_GENERIC_ACCESS_SERVICE,
@@ -394,9 +398,14 @@ async def pair(
print(color(f'=== Connecting to {address_or_name}...', 'green'))
connection = await device.connect(
address_or_name,
+<<<<<<< HEAD
transport=BT_LE_TRANSPORT if mode == 'le' else BT_BR_EDR_TRANSPORT,
)
pairing_failure = False
+=======
+ transport=(BT_LE_TRANSPORT if mode == 'le' else BT_BR_EDR_TRANSPORT),
+ )
+>>>>>>> e3de14f (first implementation (+1 squashed commit))
if not request:
try:
diff --git a/bumble/device.py b/bumble/device.py
index 45e919d..f343a4b 100644
--- a/bumble/device.py
+++ b/bumble/device.py
@@ -103,6 +103,7 @@ from .hci import (
HCI_LE_Set_Advertising_Data_Command,
HCI_LE_Set_Advertising_Enable_Command,
HCI_LE_Set_Advertising_Parameters_Command,
+ HCI_LE_Set_Data_Length_Command,
HCI_LE_Set_Default_PHY_Command,
HCI_LE_Set_Extended_Scan_Enable_Command,
HCI_LE_Set_Extended_Scan_Parameters_Command,
@@ -736,6 +737,9 @@ class Connection(CompositeEventEmitter):
self.remove_listener('disconnection', abort.set_result)
self.remove_listener('disconnection_failure', abort.set_exception)
+ async def set_data_length(self, tx_octets, tx_time) -> None:
+ return await self.device.set_data_length(self, tx_octets, tx_time)
+
async def update_parameters(
self,
connection_interval_min,
@@ -2193,6 +2197,22 @@ class Device(CompositeEventEmitter):
)
self.disconnecting = False
+ async def set_data_length(self, connection, tx_octets, tx_time) -> None:
+ if tx_octets < 0x001B or tx_octets > 0x00FB:
+ raise ValueError('tx_octets must be between 0x001B and 0x00FB')
+
+ if tx_time < 0x0148 or tx_time > 0x4290:
+ raise ValueError('tx_time must be between 0x0148 and 0x4290')
+
+ return await self.send_command(
+ HCI_LE_Set_Data_Length_Command(
+ connection_handle=connection.handle,
+ tx_octets=tx_octets,
+ tx_time=tx_time,
+ ), # type: ignore[call-arg]
+ check_result=True,
+ )
+
async def update_connection_parameters(
self,
connection,
diff --git a/docs/mkdocs/mkdocs.yml b/docs/mkdocs/mkdocs.yml
index 84e2515..6590d12 100644
--- a/docs/mkdocs/mkdocs.yml
+++ b/docs/mkdocs/mkdocs.yml
@@ -70,6 +70,7 @@ nav:
- Extras:
- extras/index.md
- Android Remote HCI: extras/android_remote_hci.md
+ - Android BT Bench: extras/android_bt_bench.md
- Hive:
- hive/index.md
- Speaker: hive/web/speaker/speaker.html
diff --git a/docs/mkdocs/src/extras/android_bt_bench.md b/docs/mkdocs/src/extras/android_bt_bench.md
new file mode 100644
index 0000000..2417e00
--- /dev/null
+++ b/docs/mkdocs/src/extras/android_bt_bench.md
@@ -0,0 +1,64 @@
+ANDROID BENCH APP
+=================
+
+This Android app that is compatible with the Bumble `bench` command line app.
+This app can be used to test the throughput and latency between two Android
+devices, or between an Android device and another device running the Bumble
+`bench` app.
+Only the RFComm Client, RFComm Server, L2CAP Client and L2CAP Server modes are
+supported.
+
+Building
+--------
+
+You can build the app by running `./gradlew build` (use `gradlew.bat` on Windows) from the `BtBench` top level directory.
+You can also build with Android Studio: open the `BtBench` project. You can build and/or debug from there.
+
+If the build succeeds, you can find the app APKs (debug and release) at:
+
+ * [Release] ``app/build/outputs/apk/release/app-release-unsigned.apk``
+ * [Debug] ``app/build/outputs/apk/debug/app-debug.apk``
+
+
+Running
+-------
+
+### Starting the app
+You can start the app from the Android launcher, from Android Studio, or with `adb`
+
+#### Launching from the launcher
+Just tap the app icon on the launcher, check the parameters, and tap
+one of the benchmark action buttons.
+
+#### Launching with `adb`
+Using the `am` command, you can start the activity, and pass it arguments so that you can
+automatically start the benchmark test, and/or set the parameters.
+
+| Parameter Name | Parameter Type | Description
+|------------------------|----------------|------------
+| autostart | String | Benchmark to start. (rfcomm-client, rfcomm-server, l2cap-client or l2cap-server)
+| packet-count | Integer | Number of packets to send (rfcomm-client and l2cap-client only)
+| packet-size | Integer | Number of bytes per packet (rfcomm-client and l2cap-client only)
+| peer-bluetooth-address | Integer | Peer Bluetooth address to connect to (rfcomm-client and l2cap-client | only)
+
+
+!!! tip "Launching from adb with auto-start"
+ In this example, we auto-start the Rfcomm Server bench action.
+ ```bash
+ $ adb shell am start -n com.github.google.bumble.btbench/.MainActivity --es autostart rfcomm-server
+ ```
+
+!!! tip "Launching from adb with auto-start and some parameters"
+ In this example, we auto-start the Rfcomm Client bench action, set the packet count to 100,
+ and the packet size to 1024, and connect to DA:4C:10:DE:17:02
+ ```bash
+ $ adb shell am start -n com.github.google.bumble.btbench/.MainActivity --es autostart rfcomm-client --ei packet-count 100 --ei packet-size 1024 --es peer-bluetooth-address DA:4C:10:DE:17:02
+ ```
+
+#### Selecting a Peer Bluetooth Address
+The app's main activity has a "Peer Bluetooth Address" setting where you can change the address.
+
+!!! note "Bluetooth Address for L2CAP vs RFComm"
+ For BLE (L2CAP mode), the address of a device typically changes regularly (it is randomized for privacy), whereas the Bluetooth Classic addresses will remain the same (RFComm mode).
+ If two devices are paired and bonded, then they will each "see" a non-changing address for each other even with BLE (Resolvable Private Address)
+
diff --git a/docs/mkdocs/src/extras/index.md b/docs/mkdocs/src/extras/index.md
index ae906c1..59af838 100644
--- a/docs/mkdocs/src/extras/index.md
+++ b/docs/mkdocs/src/extras/index.md
@@ -8,4 +8,12 @@ Android Remote HCI
Allows using an Android phone's built-in Bluetooth controller with a Bumble
stack running on a development machine.
-See [Android Remote HCI](android_remote_hci.md) for details.
\ No newline at end of file
+See [Android Remote HCI](android_remote_hci.md) for details.
+
+Android BT Bench
+----------------
+
+An Android app that is compatible with the Bumble `bench` command line app.
+This app can be used to test the throughput and latency between two Android
+devices, or between an Android device and another device running the Bumble
+`bench` app.
\ No newline at end of file
diff --git a/extras/android/BtBench/.gitignore b/extras/android/BtBench/.gitignore
new file mode 100644
index 0000000..aa724b7
--- /dev/null
+++ b/extras/android/BtBench/.gitignore
@@ -0,0 +1,15 @@
+*.iml
+.gradle
+/local.properties
+/.idea/caches
+/.idea/libraries
+/.idea/modules.xml
+/.idea/workspace.xml
+/.idea/navEditor.xml
+/.idea/assetWizardSettings.xml
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.cxx
+local.properties
diff --git a/extras/android/BtBench/app/.gitignore b/extras/android/BtBench/app/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/extras/android/BtBench/app/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/extras/android/BtBench/app/build.gradle.kts b/extras/android/BtBench/app/build.gradle.kts
new file mode 100644
index 0000000..ffde197
--- /dev/null
+++ b/extras/android/BtBench/app/build.gradle.kts
@@ -0,0 +1,70 @@
+@Suppress("DSL_SCOPE_VIOLATION") // TODO: Remove once KTIJ-19369 is fixed
+plugins {
+ alias(libs.plugins.androidApplication)
+ alias(libs.plugins.kotlinAndroid)
+}
+
+android {
+ namespace = "com.github.google.bumble.btbench"
+ compileSdk = 34
+
+ defaultConfig {
+ applicationId = "com.github.google.bumble.btbench"
+ minSdk = 30
+ targetSdk = 34
+ versionCode = 1
+ versionName = "1.0"
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ vectorDrawables {
+ useSupportLibrary = true
+ }
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = "1.8"
+ }
+ buildFeatures {
+ compose = true
+ }
+ composeOptions {
+ kotlinCompilerExtensionVersion = "1.5.1"
+ }
+ packaging {
+ resources {
+ excludes += "/META-INF/{AL2.0,LGPL2.1}"
+ }
+ }
+}
+
+dependencies {
+
+ implementation(libs.core.ktx)
+ implementation(libs.lifecycle.runtime.ktx)
+ implementation(libs.activity.compose)
+ implementation(platform(libs.compose.bom))
+ implementation(libs.ui)
+ implementation(libs.ui.graphics)
+ implementation(libs.ui.tooling.preview)
+ implementation(libs.material3)
+ testImplementation(libs.junit)
+ androidTestImplementation(libs.androidx.test.ext.junit)
+ androidTestImplementation(libs.espresso.core)
+ androidTestImplementation(platform(libs.compose.bom))
+ androidTestImplementation(libs.ui.test.junit4)
+ debugImplementation(libs.ui.tooling)
+ debugImplementation(libs.ui.test.manifest)
+}
\ No newline at end of file
diff --git a/extras/android/BtBench/app/proguard-rules.pro b/extras/android/BtBench/app/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/extras/android/BtBench/app/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/extras/android/BtBench/app/src/main/AndroidManifest.xml b/extras/android/BtBench/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..a6b5d77
--- /dev/null
+++ b/extras/android/BtBench/app/src/main/AndroidManifest.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/extras/android/BtBench/app/src/main/ic_launcher-playstore.png b/extras/android/BtBench/app/src/main/ic_launcher-playstore.png
new file mode 100644
index 0000000..d27fdd2
Binary files /dev/null and b/extras/android/BtBench/app/src/main/ic_launcher-playstore.png differ
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
new file mode 100644
index 0000000..7722bb8
--- /dev/null
+++ b/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/L2capClient.kt
@@ -0,0 +1,35 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.github.google.bumble.btbench
+
+import android.annotation.SuppressLint
+import android.bluetooth.BluetoothAdapter
+import java.io.IOException
+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) {
+ @SuppressLint("MissingPermission")
+ fun run() {
+ viewModel.running = true
+ val remoteDevice = bluetoothAdapter.getRemoteDevice(viewModel.peerBluetoothAddress)
+ val socket = remoteDevice.createInsecureL2capChannel(viewModel.l2capPsm)
+
+ val client = SocketClient(viewModel, socket)
+ client.run()
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 0000000..79c7004
--- /dev/null
+++ b/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/L2capServer.kt
@@ -0,0 +1,62 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.github.google.bumble.btbench
+
+import android.annotation.SuppressLint
+import android.bluetooth.BluetoothAdapter
+import android.bluetooth.le.AdvertiseCallback
+import android.bluetooth.le.AdvertiseData
+import android.bluetooth.le.AdvertiseSettings
+import android.bluetooth.le.AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY
+import android.os.Build
+import java.io.IOException
+import java.util.logging.Logger
+import kotlin.concurrent.thread
+
+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.
+ val callback = object: AdvertiseCallback() {
+ override fun onStartFailure(errorCode: Int) {
+ Log.warning("failed to start advertising: $errorCode")
+ }
+
+ override fun onStartSuccess(settingsInEffect: AdvertiseSettings) {
+ Log.info("advertising started: $settingsInEffect")
+ }
+ }
+ val advertiseSettingsBuilder = AdvertiseSettings.Builder()
+ .setAdvertiseMode(ADVERTISE_MODE_LOW_LATENCY)
+ .setConnectable(true)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ advertiseSettingsBuilder.setDiscoverable(true)
+ }
+ val advertiseSettings = advertiseSettingsBuilder.build()
+ 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) })
+ }
+}
\ 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
new file mode 100644
index 0000000..314f746
--- /dev/null
+++ b/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/MainActivity.kt
@@ -0,0 +1,307 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.github.google.bumble.btbench
+
+import android.Manifest
+import android.annotation.SuppressLint
+import android.bluetooth.BluetoothAdapter
+import android.bluetooth.BluetoothManager
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.os.Build
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.text.KeyboardActions
+import androidx.compose.foundation.text.KeyboardOptions
+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.Text
+import androidx.compose.material3.TextField
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalSoftwareKeyboardController
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.core.content.ContextCompat
+import com.github.google.bumble.btbench.ui.theme.BTBenchTheme
+import java.util.logging.Logger
+
+private val Log = Logger.getLogger("bumble.main-activity")
+
+const val PEER_BLUETOOTH_ADDRESS_PREF_KEY = "peer_bluetooth_address"
+const val SENDER_PACKET_COUNT_PREF_KEY = "sender_packet_count"
+const val SENDER_PACKET_SIZE_PREF_KEY = "sender_packet_size"
+
+class MainActivity : ComponentActivity() {
+ private val appViewModel = AppViewModel()
+ private var bluetoothAdapter: BluetoothAdapter? = null
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ appViewModel.loadPreferences(getPreferences(Context.MODE_PRIVATE))
+ checkPermissions()
+ }
+
+ private fun checkPermissions() {
+ val neededPermissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ arrayOf(
+ Manifest.permission.BLUETOOTH_ADVERTISE,
+ Manifest.permission.BLUETOOTH_SCAN,
+ Manifest.permission.BLUETOOTH_CONNECT
+ )
+ } else {
+ arrayOf(Manifest.permission.BLUETOOTH, Manifest.permission.BLUETOOTH_ADMIN)
+ }
+ val missingPermissions = neededPermissions.filter {
+ ContextCompat.checkSelfPermission(baseContext, it) != PackageManager.PERMISSION_GRANTED
+ }
+
+ if (missingPermissions.isEmpty()) {
+ start()
+ return
+ }
+
+ val requestPermissionsLauncher = registerForActivityResult(
+ ActivityResultContracts.RequestMultiplePermissions()
+ ) { permissions ->
+ permissions.entries.forEach {
+ Log.info("permission: ${it.key} = ${it.value}")
+ }
+ val grantCount = permissions.count { it.value }
+ if (grantCount == neededPermissions.size) {
+ // We have all the permissions we need.
+ start()
+ } else {
+ Log.warning("not all permissions granted")
+ }
+ }
+
+ requestPermissionsLauncher.launch(missingPermissions.toTypedArray())
+ return
+ }
+
+ @SuppressLint("MissingPermission")
+ private fun initBluetooth() {
+ val bluetoothManager = ContextCompat.getSystemService(this, BluetoothManager::class.java)
+ bluetoothAdapter = bluetoothManager?.adapter
+
+ if (bluetoothAdapter == null) {
+ Log.warning("no bluetooth adapter")
+ return
+ }
+
+ if (!bluetoothAdapter!!.isEnabled) {
+ Log.warning("bluetooth not enabled")
+ return
+ }
+ }
+
+ private fun start() {
+ initBluetooth()
+ setContent {
+ MainView(
+ appViewModel,
+ ::becomeDiscoverable,
+ ::runRfcommClient,
+ ::runRfcommServer,
+ ::runL2capClient,
+ ::runL2capServer
+ )
+ }
+
+ // Process intent parameters, if any.
+ intent.getStringExtra("peer-bluetooth-address")?.let {
+ appViewModel.peerBluetoothAddress = it
+ }
+ val packetCount = intent.getIntExtra("packet-count", 0)
+ if (packetCount > 0) {
+ appViewModel.senderPacketCount = packetCount
+ }
+ appViewModel.updateSenderPacketCountSlider()
+ val packetSize = intent.getIntExtra("packet-size", 0)
+ if (packetSize > 0) {
+ appViewModel.senderPacketSize = packetSize
+ }
+ appViewModel.updateSenderPacketSizeSlider()
+ intent.getStringExtra("autostart")?.let {
+ when (it) {
+ "rfcomm-client" -> runRfcommClient()
+ "rfcomm-server" -> runRfcommServer()
+ "l2cap-client" -> runL2capClient()
+ "l2cap-server" -> runL2capServer()
+ }
+ }
+ }
+
+ private fun runRfcommClient() {
+ val rfcommClient = bluetoothAdapter?.let { RfcommClient(appViewModel, it) }
+ rfcommClient?.run()
+ }
+
+ private fun runRfcommServer() {
+ val rfcommServer = bluetoothAdapter?.let { RfcommServer(appViewModel, it) }
+ rfcommServer?.run()
+ }
+
+ private fun runL2capClient() {
+ val l2capClient = bluetoothAdapter?.let { L2capClient(appViewModel, it) }
+ l2capClient?.run()
+ }
+
+ private fun runL2capServer() {
+ val l2capServer = bluetoothAdapter?.let { L2capServer(appViewModel, it) }
+ l2capServer?.run()
+ }
+
+ @SuppressLint("MissingPermission")
+ fun becomeDiscoverable() {
+ val discoverableIntent = Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE)
+ discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300)
+ startActivity(discoverableIntent)
+ }
+}
+
+@OptIn(ExperimentalComposeUiApi::class)
+@Composable
+fun MainView(
+ appViewModel: AppViewModel,
+ becomeDiscoverable: () -> Unit,
+ runRfcommClient: () -> Unit,
+ runRfcommServer: () -> Unit,
+ runL2capClient: () -> Unit,
+ runL2capServer: () -> Unit
+) {
+ BTBenchTheme {
+ // A surface container using the 'background' color from the theme
+ Surface(
+ modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background
+ ) {
+ Column(modifier = Modifier.padding(horizontal = 16.dp)) {
+ Text(
+ text = "Bumble Bench",
+ fontSize = 24.sp,
+ fontWeight = FontWeight.Bold,
+ textAlign = TextAlign.Center
+ )
+ Divider()
+ val keyboardController = LocalSoftwareKeyboardController.current
+ TextField(label = {
+ Text(text = "Peer Bluetooth Address")
+ },
+ value = appViewModel.peerBluetoothAddress,
+ modifier = Modifier.fillMaxWidth(),
+ keyboardOptions = KeyboardOptions.Default.copy(
+ keyboardType = KeyboardType.Ascii, imeAction = ImeAction.Done
+ ),
+ onValueChange = {
+ appViewModel.updatePeerBluetoothAddress(it)
+ },
+ keyboardActions = KeyboardActions(onDone = { keyboardController?.hide() })
+ )
+ Divider()
+ TextField(label = {
+ Text(text = "L2CAP PSM")
+ },
+ value = appViewModel.l2capPsm.toString(),
+ modifier = Modifier.fillMaxWidth(),
+ keyboardOptions = KeyboardOptions.Default.copy(
+ keyboardType = KeyboardType.Number,
+ imeAction = ImeAction.Done
+ ),
+ onValueChange = {
+ if (it.isNotEmpty()) {
+ val psm = it.toIntOrNull()
+ if (psm != null) {
+ appViewModel.l2capPsm = psm
+ }
+ }
+ },
+ keyboardActions = KeyboardActions(onDone = { keyboardController?.hide() }))
+ Divider()
+ Slider(
+ value = appViewModel.senderPacketCountSlider, onValueChange = {
+ appViewModel.senderPacketCountSlider = it
+ appViewModel.updateSenderPacketCount()
+ }, steps = 4
+ )
+ Text(text = "Packet Count: " + appViewModel.senderPacketCount.toString())
+ Divider()
+ Slider(
+ value = appViewModel.senderPacketSizeSlider, onValueChange = {
+ appViewModel.senderPacketSizeSlider = it
+ appViewModel.updateSenderPacketSize()
+ }, steps = 4
+ )
+ Text(text = "Packet Size: " + appViewModel.senderPacketSize.toString())
+ Divider()
+ ActionButton(
+ text = "Become Discoverable", onClick = becomeDiscoverable, true
+ )
+ Row() {
+ ActionButton(
+ text = "RFCOMM Client", onClick = runRfcommClient, !appViewModel.running
+ )
+ ActionButton(
+ text = "RFCOMM Server", onClick = runRfcommServer, !appViewModel.running
+ )
+ }
+ Row() {
+ ActionButton(
+ text = "L2CAP Client", onClick = runL2capClient, !appViewModel.running
+ )
+ ActionButton(
+ text = "L2CAP Server", onClick = runL2capServer, !appViewModel.running
+ )
+ }
+ Divider()
+ Text(
+ text = "Packets Sent: ${appViewModel.packetsSent}"
+ )
+ Text(
+ text = "Packets Received: ${appViewModel.packetsReceived}"
+ )
+ Text(
+ text = "Throughput: ${appViewModel.throughput}"
+ )
+ Divider()
+ ActionButton(
+ text = "Abort", onClick = appViewModel::abort, appViewModel.running
+ )
+ }
+ }
+ }
+}
+
+@Composable
+fun ActionButton(text: String, onClick: () -> Unit, enabled: Boolean) {
+ Button(onClick = onClick, enabled = enabled) {
+ Text(text = text)
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 0000000..3a62519
--- /dev/null
+++ b/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/Model.kt
@@ -0,0 +1,163 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.github.google.bumble.btbench
+
+import android.content.SharedPreferences
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.lifecycle.ViewModel
+import java.util.UUID
+
+val DEFAULT_RFCOMM_UUID = UUID.fromString("0AF17D61-5DAE-4530-BE0D-6A8D54C3608B")
+const val DEFAULT_PEER_BLUETOOTH_ADDRESS = "AA:BB:CC:DD:EE:FF"
+const val DEFAULT_SENDER_PACKET_COUNT = 100
+const val DEFAULT_SENDER_PACKET_SIZE = 1024
+
+class AppViewModel : ViewModel() {
+ private var preferences: SharedPreferences? = null
+ var peerBluetoothAddress by mutableStateOf(DEFAULT_PEER_BLUETOOTH_ADDRESS)
+ var l2capPsm by mutableStateOf(0)
+ var senderPacketCountSlider by mutableFloatStateOf(0.0F)
+ var senderPacketSizeSlider by mutableFloatStateOf(0.0F)
+ var senderPacketCount by mutableIntStateOf(DEFAULT_SENDER_PACKET_COUNT)
+ var senderPacketSize by mutableIntStateOf(DEFAULT_SENDER_PACKET_SIZE)
+ var packetsSent by mutableIntStateOf(0)
+ var packetsReceived by mutableIntStateOf(0)
+ var throughput by mutableIntStateOf(0)
+ var running by mutableStateOf(false)
+ var aborter: (() -> Unit)? = null
+
+ fun loadPreferences(preferences: SharedPreferences) {
+ this.preferences = preferences
+
+ val savedPeerBluetoothAddress = preferences.getString(PEER_BLUETOOTH_ADDRESS_PREF_KEY, null)
+ if (savedPeerBluetoothAddress != null) {
+ peerBluetoothAddress = savedPeerBluetoothAddress
+ }
+
+ val savedSenderPacketCount = preferences.getInt(SENDER_PACKET_COUNT_PREF_KEY, 0)
+ if (savedSenderPacketCount != 0) {
+ senderPacketCount = savedSenderPacketCount
+ }
+ updateSenderPacketCountSlider()
+
+ val savedSenderPacketSize = preferences.getInt(SENDER_PACKET_SIZE_PREF_KEY, 0)
+ if (savedSenderPacketSize != 0) {
+ senderPacketSize = savedSenderPacketSize
+ }
+ updateSenderPacketSizeSlider()
+ }
+
+ fun updatePeerBluetoothAddress(peerBluetoothAddress: String) {
+ this.peerBluetoothAddress = peerBluetoothAddress
+
+ // Save the address to the preferences
+ with(preferences!!.edit()) {
+ putString(PEER_BLUETOOTH_ADDRESS_PREF_KEY, peerBluetoothAddress)
+ apply()
+ }
+ }
+
+ fun updateSenderPacketCountSlider() {
+ if (senderPacketCount <= 10) {
+ senderPacketCountSlider = 0.0F
+ } else if (senderPacketCount <= 50) {
+ senderPacketCountSlider = 0.2F
+ } else if (senderPacketCount <= 100) {
+ senderPacketCountSlider = 0.4F
+ } else if (senderPacketCount <= 500) {
+ senderPacketCountSlider = 0.6F
+ } else if (senderPacketCount <= 1000) {
+ senderPacketCountSlider = 0.8F
+ } else {
+ senderPacketCountSlider = 1.0F
+ }
+
+ with(preferences!!.edit()) {
+ putInt(SENDER_PACKET_COUNT_PREF_KEY, senderPacketCount)
+ apply()
+ }
+ }
+
+ fun updateSenderPacketCount() {
+ if (senderPacketCountSlider < 0.2) {
+ senderPacketCount = 10
+ } else if (senderPacketCountSlider < 0.4) {
+ senderPacketCount = 50
+ } else if (senderPacketCountSlider < 0.6) {
+ senderPacketCount = 100
+ } else if (senderPacketCountSlider < 0.8) {
+ senderPacketCount = 500
+ } else if (senderPacketCountSlider < 1.0) {
+ senderPacketCount = 1000
+ } else {
+ senderPacketCount = 10000
+ }
+
+ with(preferences!!.edit()) {
+ putInt(SENDER_PACKET_COUNT_PREF_KEY, senderPacketCount)
+ apply()
+ }
+ }
+
+ fun updateSenderPacketSizeSlider() {
+ if (senderPacketSize <= 1) {
+ senderPacketSizeSlider = 0.0F
+ } else if (senderPacketSize <= 256) {
+ senderPacketSizeSlider = 0.02F
+ } else if (senderPacketSize <= 512) {
+ senderPacketSizeSlider = 0.4F
+ } else if (senderPacketSize <= 1024) {
+ senderPacketSizeSlider = 0.6F
+ } else if (senderPacketSize <= 2048) {
+ senderPacketSizeSlider = 0.8F
+ } else {
+ senderPacketSizeSlider = 1.0F
+ }
+
+ with(preferences!!.edit()) {
+ putInt(SENDER_PACKET_SIZE_PREF_KEY, senderPacketSize)
+ apply()
+ }
+ }
+
+ fun updateSenderPacketSize() {
+ if (senderPacketSizeSlider < 0.2) {
+ senderPacketSize = 1
+ } else if (senderPacketSizeSlider < 0.4) {
+ senderPacketSize = 256
+ } else if (senderPacketSizeSlider < 0.6) {
+ senderPacketSize = 512
+ } else if (senderPacketSizeSlider < 0.8) {
+ senderPacketSize = 1024
+ } else if (senderPacketSizeSlider < 1.0) {
+ senderPacketSize = 2048
+ } else {
+ senderPacketSize = 4096
+ }
+
+ with(preferences!!.edit()) {
+ putInt(SENDER_PACKET_SIZE_PREF_KEY, senderPacketSize)
+ apply()
+ }
+ }
+
+ fun abort() {
+ aborter?.let { it() }
+ }
+}
diff --git a/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/Packet.kt b/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/Packet.kt
new file mode 100644
index 0000000..0fa8500
--- /dev/null
+++ b/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/Packet.kt
@@ -0,0 +1,178 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.github.google.bumble.btbench
+
+import android.bluetooth.BluetoothSocket
+import java.io.IOException
+import java.nio.ByteBuffer
+import java.util.logging.Logger
+import kotlin.math.min
+
+private val Log = Logger.getLogger("btbench.packet")
+
+fun ByteArray.toHex(): String = joinToString(separator = "") { eachByte -> "%02x".format(eachByte) }
+
+abstract class Packet(val type: Int, val payload: ByteArray = ByteArray(0)) {
+ companion object {
+ const val RESET = 0
+ const val SEQUENCE = 1
+ const val ACK = 2
+
+ const val LAST_FLAG = 1
+
+ fun from(data: ByteArray): Packet {
+ return when (data[0].toInt()) {
+ RESET -> ResetPacket()
+ SEQUENCE -> SequencePacket(
+ data[1].toInt(),
+ ByteBuffer.wrap(data, 2, 4).getInt(),
+ data.sliceArray(6.. AckPacket(data[1].toInt(), ByteBuffer.wrap(data, 2, 4).getInt())
+ else -> GenericPacket(data[0].toInt(), data.sliceArray(1.. onResetPacket()
+ is AckPacket -> onAckPacket()
+ is SequencePacket -> onSequencePacket(packet)
+ }
+ }
+
+ abstract fun onResetPacket()
+ abstract fun onAckPacket()
+ abstract fun onSequencePacket(packet: SequencePacket)
+}
+
+interface DataSink {
+ fun onData(data: ByteArray)
+}
+
+interface PacketIO {
+ var packetSink: PacketSink?
+ fun sendPacket(packet: Packet)
+}
+
+class StreamedPacketIO(private val dataSink: DataSink) : PacketIO {
+ private var bytesNeeded: Int = 0
+ private var rxPacket: ByteBuffer? = null
+ private var rxHeader = ByteBuffer.allocate(2)
+
+ override var packetSink: PacketSink? = null
+
+ fun onData(data: ByteArray) {
+ var current = data
+ while (current.isNotEmpty()) {
+ if (bytesNeeded > 0) {
+ val chunk = current.sliceArray(0.. Unit
+) {
+ fun receive() {
+ val buffer = ByteArray(4096)
+ do {
+ try {
+ val bytesRead = socket.inputStream.read(buffer)
+ if (bytesRead <= 0) {
+ break
+ }
+ onData(buffer.sliceArray(0.. Unit) {
+ var aborted = false
+ viewModel.running = true
+
+ fun cleanup() {
+ serverSocket.close()
+ viewModel.running = false
+ onTerminate()
+ }
+
+ thread(name = "SocketServer") {
+ while (!aborted) {
+ viewModel.aborter = {
+ serverSocket.close()
+ }
+ Log.info("waiting for connection...")
+ val socket = try {
+ serverSocket.accept()
+ } catch (error: IOException) {
+ Log.warning("server socket closed")
+ cleanup()
+ return@thread
+ }
+ Log.info("got connection")
+
+ viewModel.aborter = {
+ aborted = true
+ socket.close()
+ }
+ viewModel.peerBluetoothAddress = socket.remoteDevice.address
+
+ val socketDataSink = SocketDataSink(socket)
+ val streamIO = StreamedPacketIO(socketDataSink)
+ val socketDataSource = SocketDataSource(socket, streamIO::onData)
+ val receiver = Receiver(viewModel, streamIO)
+ socketDataSource.receive()
+ socket.close()
+ }
+ cleanup()
+ }
+ }
+}
\ No newline at end of file
diff --git a/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/ui/theme/Color.kt b/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/ui/theme/Color.kt
new file mode 100644
index 0000000..2b538c8
--- /dev/null
+++ b/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/ui/theme/Color.kt
@@ -0,0 +1,11 @@
+package com.github.google.bumble.btbench.ui.theme
+
+import androidx.compose.ui.graphics.Color
+
+val Purple80 = Color(0xFFD0BCFF)
+val PurpleGrey80 = Color(0xFFCCC2DC)
+val Pink80 = Color(0xFFEFB8C8)
+
+val Purple40 = Color(0xFF6650a4)
+val PurpleGrey40 = Color(0xFF625b71)
+val Pink40 = Color(0xFF7D5260)
\ No newline at end of file
diff --git a/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/ui/theme/Theme.kt b/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/ui/theme/Theme.kt
new file mode 100644
index 0000000..1751579
--- /dev/null
+++ b/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/ui/theme/Theme.kt
@@ -0,0 +1,63 @@
+package com.github.google.bumble.btbench.ui.theme
+
+import android.app.Activity
+import android.os.Build
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.dynamicDarkColorScheme
+import androidx.compose.material3.dynamicLightColorScheme
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.SideEffect
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalView
+import androidx.core.view.WindowCompat
+
+private val DarkColorScheme = darkColorScheme(
+ primary = Purple80, secondary = PurpleGrey80, tertiary = Pink80
+)
+
+private val LightColorScheme = lightColorScheme(
+ primary = Purple40, secondary = PurpleGrey40, tertiary = Pink40
+
+ /* Other default colors to override
+ background = Color(0xFFFFFBFE),
+ surface = Color(0xFFFFFBFE),
+ onPrimary = Color.White,
+ onSecondary = Color.White,
+ onTertiary = Color.White,
+ onBackground = Color(0xFF1C1B1F),
+ onSurface = Color(0xFF1C1B1F),
+ */
+)
+
+@Composable
+fun BTBenchTheme(
+ darkTheme: Boolean = isSystemInDarkTheme(),
+ // Dynamic color is available on Android 12+
+ dynamicColor: Boolean = true, content: @Composable () -> Unit
+) {
+ val colorScheme = when {
+ dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
+ val context = LocalContext.current
+ if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
+ }
+
+ darkTheme -> DarkColorScheme
+ else -> LightColorScheme
+ }
+ val view = LocalView.current
+ if (!view.isInEditMode) {
+ SideEffect {
+ val window = (view.context as Activity).window
+ window.statusBarColor = colorScheme.primary.toArgb()
+ WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
+ }
+ }
+
+ MaterialTheme(
+ colorScheme = colorScheme, typography = Typography, content = content
+ )
+}
\ No newline at end of file
diff --git a/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/ui/theme/Type.kt b/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/ui/theme/Type.kt
new file mode 100644
index 0000000..029f898
--- /dev/null
+++ b/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/ui/theme/Type.kt
@@ -0,0 +1,33 @@
+package com.github.google.bumble.btbench.ui.theme
+
+import androidx.compose.material3.Typography
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.sp
+
+// Set of Material typography styles to start with
+val Typography = Typography(
+ bodyLarge = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 16.sp,
+ lineHeight = 24.sp,
+ letterSpacing = 0.5.sp
+ )/* Other default text styles to override
+ titleLarge = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 22.sp,
+ lineHeight = 28.sp,
+ letterSpacing = 0.sp
+ ),
+ labelSmall = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Medium,
+ fontSize = 11.sp,
+ lineHeight = 16.sp,
+ letterSpacing = 0.5.sp
+ )
+ */
+)
\ No newline at end of file
diff --git a/extras/android/BtBench/app/src/main/res/drawable/ic_launcher_background.xml b/extras/android/BtBench/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..ca3826a
--- /dev/null
+++ b/extras/android/BtBench/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,74 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/extras/android/BtBench/app/src/main/res/drawable/ic_launcher_foreground.xml b/extras/android/BtBench/app/src/main/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 0000000..2b068d1
--- /dev/null
+++ b/extras/android/BtBench/app/src/main/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/extras/android/BtBench/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/extras/android/BtBench/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..036d09b
--- /dev/null
+++ b/extras/android/BtBench/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/extras/android/BtBench/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/extras/android/BtBench/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..036d09b
--- /dev/null
+++ b/extras/android/BtBench/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/extras/android/BtBench/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/extras/android/BtBench/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp
new file mode 100644
index 0000000..7dc4135
Binary files /dev/null and b/extras/android/BtBench/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp differ
diff --git a/extras/android/BtBench/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/extras/android/BtBench/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..37c0b56
Binary files /dev/null and b/extras/android/BtBench/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/extras/android/BtBench/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/extras/android/BtBench/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp
new file mode 100644
index 0000000..e8f5332
Binary files /dev/null and b/extras/android/BtBench/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp differ
diff --git a/extras/android/BtBench/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/extras/android/BtBench/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..ac1ae9b
Binary files /dev/null and b/extras/android/BtBench/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/extras/android/BtBench/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/extras/android/BtBench/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp
new file mode 100644
index 0000000..5e12fc6
Binary files /dev/null and b/extras/android/BtBench/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp differ
diff --git a/extras/android/BtBench/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/extras/android/BtBench/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..19ac4bf
Binary files /dev/null and b/extras/android/BtBench/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/extras/android/BtBench/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/extras/android/BtBench/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp
new file mode 100644
index 0000000..30516ad
Binary files /dev/null and b/extras/android/BtBench/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp differ
diff --git a/extras/android/BtBench/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/extras/android/BtBench/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..7a39c13
Binary files /dev/null and b/extras/android/BtBench/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/extras/android/BtBench/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/extras/android/BtBench/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp
new file mode 100644
index 0000000..a2b1c8b
Binary files /dev/null and b/extras/android/BtBench/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp differ
diff --git a/extras/android/BtBench/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/extras/android/BtBench/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..2bbc83f
Binary files /dev/null and b/extras/android/BtBench/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/extras/android/BtBench/app/src/main/res/values/colors.xml b/extras/android/BtBench/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..f8c6127
--- /dev/null
+++ b/extras/android/BtBench/app/src/main/res/values/colors.xml
@@ -0,0 +1,10 @@
+
+
+ #FFBB86FC
+ #FF6200EE
+ #FF3700B3
+ #FF03DAC5
+ #FF018786
+ #FF000000
+ #FFFFFFFF
+
\ No newline at end of file
diff --git a/extras/android/BtBench/app/src/main/res/values/ic_launcher_background.xml b/extras/android/BtBench/app/src/main/res/values/ic_launcher_background.xml
new file mode 100644
index 0000000..c5d5899
--- /dev/null
+++ b/extras/android/BtBench/app/src/main/res/values/ic_launcher_background.xml
@@ -0,0 +1,4 @@
+
+
+ #FFFFFF
+
\ No newline at end of file
diff --git a/extras/android/BtBench/app/src/main/res/values/strings.xml b/extras/android/BtBench/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..018c3f9
--- /dev/null
+++ b/extras/android/BtBench/app/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ BT Bench
+
\ No newline at end of file
diff --git a/extras/android/BtBench/app/src/main/res/values/themes.xml b/extras/android/BtBench/app/src/main/res/values/themes.xml
new file mode 100644
index 0000000..f0d08db
--- /dev/null
+++ b/extras/android/BtBench/app/src/main/res/values/themes.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/extras/android/BtBench/app/src/main/res/xml/backup_rules.xml b/extras/android/BtBench/app/src/main/res/xml/backup_rules.xml
new file mode 100644
index 0000000..fa0f996
--- /dev/null
+++ b/extras/android/BtBench/app/src/main/res/xml/backup_rules.xml
@@ -0,0 +1,13 @@
+
+
+
+
\ No newline at end of file
diff --git a/extras/android/BtBench/app/src/main/res/xml/data_extraction_rules.xml b/extras/android/BtBench/app/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 0000000..9ee9997
--- /dev/null
+++ b/extras/android/BtBench/app/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/extras/android/BtBench/build.gradle.kts b/extras/android/BtBench/build.gradle.kts
new file mode 100644
index 0000000..20d87a7
--- /dev/null
+++ b/extras/android/BtBench/build.gradle.kts
@@ -0,0 +1,7 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+@Suppress("DSL_SCOPE_VIOLATION") // TODO: Remove once KTIJ-19369 is fixed
+plugins {
+ alias(libs.plugins.androidApplication) apply false
+ alias(libs.plugins.kotlinAndroid) apply false
+}
+true // Needed to make the Suppress annotation work for the plugins block
\ No newline at end of file
diff --git a/extras/android/BtBench/gradle.properties b/extras/android/BtBench/gradle.properties
new file mode 100644
index 0000000..3c5031e
--- /dev/null
+++ b/extras/android/BtBench/gradle.properties
@@ -0,0 +1,23 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official
+# Enables namespacing of each library's R class so that its R class includes only the
+# resources declared in the library itself and none from the library's dependencies,
+# thereby reducing the size of the R class for that library
+android.nonTransitiveRClass=true
\ No newline at end of file
diff --git a/extras/android/BtBench/gradle/libs.versions.toml b/extras/android/BtBench/gradle/libs.versions.toml
new file mode 100644
index 0000000..03d3e58
--- /dev/null
+++ b/extras/android/BtBench/gradle/libs.versions.toml
@@ -0,0 +1,31 @@
+[versions]
+agp = "8.3.0-alpha11"
+kotlin = "1.9.0"
+core-ktx = "1.12.0"
+junit = "4.13.2"
+androidx-test-ext-junit = "1.1.5"
+espresso-core = "3.5.1"
+lifecycle-runtime-ktx = "2.6.2"
+activity-compose = "1.7.2"
+compose-bom = "2023.08.00"
+
+[libraries]
+core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "core-ktx" }
+junit = { group = "junit", name = "junit", version.ref = "junit" }
+androidx-test-ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-ext-junit" }
+espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso-core" }
+lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle-runtime-ktx" }
+activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activity-compose" }
+compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" }
+ui = { group = "androidx.compose.ui", name = "ui" }
+ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
+ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
+ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
+ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
+ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
+material3 = { group = "androidx.compose.material3", name = "material3" }
+
+[plugins]
+androidApplication = { id = "com.android.application", version.ref = "agp" }
+kotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
+
diff --git a/extras/android/BtBench/gradle/wrapper/gradle-wrapper.jar b/extras/android/BtBench/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..e708b1c
Binary files /dev/null and b/extras/android/BtBench/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/extras/android/BtBench/gradle/wrapper/gradle-wrapper.properties b/extras/android/BtBench/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..8ef5972
--- /dev/null
+++ b/extras/android/BtBench/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Wed Oct 25 07:40:52 PDT 2023
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/extras/android/BtBench/gradlew b/extras/android/BtBench/gradlew
new file mode 100755
index 0000000..4f906e0
--- /dev/null
+++ b/extras/android/BtBench/gradlew
@@ -0,0 +1,185 @@
+#!/usr/bin/env sh
+
+#
+# Copyright 2015 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=`expr $i + 1`
+ done
+ case $i in
+ 0) set -- ;;
+ 1) set -- "$args0" ;;
+ 2) set -- "$args0" "$args1" ;;
+ 3) set -- "$args0" "$args1" "$args2" ;;
+ 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=`save "$@"`
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+exec "$JAVACMD" "$@"
diff --git a/extras/android/BtBench/gradlew.bat b/extras/android/BtBench/gradlew.bat
new file mode 100644
index 0000000..ac1b06f
--- /dev/null
+++ b/extras/android/BtBench/gradlew.bat
@@ -0,0 +1,89 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/extras/android/BtBench/settings.gradle.kts b/extras/android/BtBench/settings.gradle.kts
new file mode 100644
index 0000000..9bdd1ab
--- /dev/null
+++ b/extras/android/BtBench/settings.gradle.kts
@@ -0,0 +1,24 @@
+pluginManagement {
+ repositories {
+ google {
+ content {
+ includeGroupByRegex("com\\.android.*")
+ includeGroupByRegex("com\\.google.*")
+ includeGroupByRegex("androidx.*")
+ }
+ }
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+rootProject.name = "BT Bench"
+include(":app")
+
\ No newline at end of file
diff --git a/extras/android/RemoteHCI/gradle/libs.versions.toml b/extras/android/RemoteHCI/gradle/libs.versions.toml
index a9f80cf..1350f22 100644
--- a/extras/android/RemoteHCI/gradle/libs.versions.toml
+++ b/extras/android/RemoteHCI/gradle/libs.versions.toml
@@ -1,5 +1,9 @@
[versions]
+<<<<<<< HEAD
agp = "8.3.0-alpha11"
+=======
+agp = "8.3.0-alpha10"
+>>>>>>> fd85fd8 (wip)
kotlin = "1.8.10"
core-ktx = "1.9.0"
junit = "4.13.2"