Loading android/ChannelSoundingTestApp/app/build.gradle.kts +6 −3 Original line number Diff line number Diff line Loading @@ -10,8 +10,8 @@ android { applicationId = "com.android.bluetooth.channelsoundingtestapp" minSdk = 34 targetSdk = 34 versionCode = 1 versionName = "1.0" versionCode = 2 versionName = "2.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } Loading @@ -38,6 +38,9 @@ dependencies { implementation(libs.constraintlayout) implementation(libs.navigation.fragment) implementation(libs.navigation.ui) implementation(libs.legacy.support.v4) implementation(libs.lifecycle.livedata.ktx) implementation(libs.lifecycle.viewmodel.ktx) testImplementation(libs.junit) androidTestImplementation(libs.ext.junit) androidTestImplementation(libs.espresso.core) Loading android/ChannelSoundingTestApp/app/src/main/AndroidManifest.xml +5 −2 Original line number Diff line number Diff line Loading @@ -4,9 +4,12 @@ package="com.android.bluetooth.channelsoundingtestapp"> <uses-feature android:name="android.hardware.bluetooth" android:required="true"/> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" /> <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" /> <uses-permission android:name="android.permission.BLUETOOTH_PRIVILEGED" /> <uses-permission android:name="android.permission.BLUETOOTH_SCAN" /> <application android:allowBackup="true" Loading @@ -17,7 +20,7 @@ android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.ChannelSoundingTestApp" tools:targetApi="31"> tools:targetApi="34"> <activity android:name=".MainActivity" android:exported="true" Loading android/ChannelSoundingTestApp/app/src/main/java/com/android/bluetooth/channelsoundingtestapp/BleConnectionFragment.java 0 → 100644 +162 −0 Original line number Diff line number Diff line /* * Copyright 2024 The Android Open Source Project * * 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 * * http://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.android.bluetooth.channelsoundingtestapp; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.AdapterView.OnItemSelectedListener; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.Spinner; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import java.util.ArrayList; /** Child fragment to handle BLE GATT connection. */ @SuppressWarnings("SetTextI18n") public class BleConnectionFragment extends Fragment { private BleConnectionViewModel mViewModel; private Button mBtnAdvertising; private ArrayAdapter<String> mBondedBtDevicesArrayAdapter; private Button mButtonUpdate; private Button mButtonGatt; private Button mButtonScanConnect; private Spinner mSpinnerBtAddress; public static BleConnectionFragment newInstance() { return new BleConnectionFragment(); } @Override public View onCreateView( @NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View root = inflater.inflate(R.layout.fragment_ble_connection, container, false); mBtnAdvertising = root.findViewById(R.id.btn_advertising); mButtonUpdate = (Button) root.findViewById(R.id.btn_update_devices); mButtonGatt = (Button) root.findViewById(R.id.btn_connect_gatt); mButtonScanConnect = (Button) root.findViewById(R.id.btn_scan_connect); mSpinnerBtAddress = (Spinner) root.findViewById(R.id.spinner_bt_address); return root; } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); mBondedBtDevicesArrayAdapter = new ArrayAdapter<String>( getContext(), android.R.layout.simple_spinner_item, new ArrayList<>()); mBondedBtDevicesArrayAdapter.setDropDownViewResource( android.R.layout.simple_spinner_dropdown_item); mSpinnerBtAddress.setAdapter(mBondedBtDevicesArrayAdapter); mViewModel = new ViewModelProvider(requireParentFragment()).get(BleConnectionViewModel.class); mViewModel .getGattState() .observe( getActivity(), gattSate -> { switch (gattSate) { case CONNECTED_DIRECT: mButtonGatt.setText("Disconnect Gatt"); break; case SCANNING: mButtonScanConnect.setText("Stop Scan"); break; case CONNECTED_SCAN: mButtonScanConnect.setText("Disconnect Gatt"); break; case DISCONNECTED: default: mButtonGatt.setText("Connect Gatt"); mButtonScanConnect.setText("Scan and Connect"); } }); mButtonUpdate.setOnClickListener( v -> { mViewModel.updateBondedDevices(); }); mViewModel .getBondedBtDeviceAddresses() .observe( getActivity(), deviceList -> { mBondedBtDevicesArrayAdapter.clear(); mBondedBtDevicesArrayAdapter.addAll(deviceList); if (mSpinnerBtAddress.getSelectedItem() != null) { String selectedBtAddress = mSpinnerBtAddress.getSelectedItem().toString(); mViewModel.setCsTargetAddress(selectedBtAddress); } }); mSpinnerBtAddress.setOnItemSelectedListener( new OnItemSelectedListener() { @Override public void onItemSelected( AdapterView<?> adapterView, View view, int i, long l) { String btAddress = mSpinnerBtAddress.getSelectedItem().toString(); mViewModel.setCsTargetAddress(btAddress); } @Override public void onNothingSelected(AdapterView<?> adapterView) { mViewModel.setCsTargetAddress(""); } }); mButtonGatt.setOnClickListener( v -> { mViewModel.toggleGattConnection(); }); mButtonScanConnect.setOnClickListener( v -> { mViewModel.toggleScanConnect(); }); mViewModel .getIsAdvertising() .observe( getActivity(), isAdvertising -> { if (isAdvertising) { mBtnAdvertising.setText("Stop Advertising"); } else { mBtnAdvertising.setText("Start Advertising"); } }); mBtnAdvertising.setOnClickListener( new OnClickListener() { @Override public void onClick(View view) { mViewModel.toggleAdvertising(); } }); } } android/ChannelSoundingTestApp/app/src/main/java/com/android/bluetooth/channelsoundingtestapp/BleConnectionViewModel.java 0 → 100644 +342 −0 Original line number Diff line number Diff line /* * Copyright 2024 The Android Open Source Project * * 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 * * http://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.android.bluetooth.channelsoundingtestapp; import android.annotation.SuppressLint; import android.app.Application; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCallback; import android.bluetooth.BluetoothGattServer; import android.bluetooth.BluetoothGattServerCallback; import android.bluetooth.BluetoothManager; import android.bluetooth.BluetoothProfile; import android.bluetooth.le.AdvertiseData; import android.bluetooth.le.AdvertisingSet; import android.bluetooth.le.AdvertisingSetCallback; import android.bluetooth.le.AdvertisingSetParameters; import android.bluetooth.le.BluetoothLeAdvertiser; import android.bluetooth.le.BluetoothLeScanner; import android.bluetooth.le.ScanCallback; import android.bluetooth.le.ScanFilter; import android.bluetooth.le.ScanResult; import android.bluetooth.le.ScanSettings; import android.os.ParcelUuid; import android.text.TextUtils; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import com.android.bluetooth.channelsoundingtestapp.Constants.GattState; import java.util.ArrayList; import java.util.List; import java.util.Set; /** The ViewModel for the BLE GATT connection. */ @SuppressLint("MissingPermission") // permissions are checked upfront public class BleConnectionViewModel extends AndroidViewModel { private static final int GATT_MTU_SIZE = 512; private final BluetoothAdapter mBluetoothAdapter; private final BluetoothManager mBluetoothManager; @Nullable private BluetoothGatt mBluetoothGatt = null; private MutableLiveData<Boolean> mIsAdvertising = new MutableLiveData<>(false); private MutableLiveData<String> mLogText = new MutableLiveData<>(); private MutableLiveData<BluetoothDevice> mTargetDevice = new MutableLiveData<>(); // scanner private final MutableLiveData<List<String>> mBondedBtDeviceAddresses = new MutableLiveData<>(); private final MutableLiveData<GattState> mGattState = new MutableLiveData<>(GattState.DISCONNECTED); private String mTargetBtAddress = ""; private GattState mExpectedGattState = GattState.DISCONNECTED; /** Constructor */ public BleConnectionViewModel(@NonNull Application application) { super(application); mBluetoothManager = application.getSystemService(BluetoothManager.class); mBluetoothAdapter = mBluetoothManager.getAdapter(); } LiveData<Boolean> getIsAdvertising() { return mIsAdvertising; } LiveData<String> getLogText() { return mLogText; } LiveData<GattState> getGattState() { return mGattState; } LiveData<List<String>> getBondedBtDeviceAddresses() { return mBondedBtDeviceAddresses; } LiveData<BluetoothDevice> getTargetDevice() { return mTargetDevice; } void toggleAdvertising() { if (mIsAdvertising.getValue()) { stopAdvertising(); } else { startConnectableAdvertising(); } } AdvertisingSetCallback mAdvertisingSetCallback = new AdvertisingSetCallback() { @Override public void onAdvertisingSetStarted( AdvertisingSet advertisingSet, int txPower, int status) { printLog( "onAdvertisingSetStarted(): txPower:" + txPower + " , status: " + status); if (status == 0) { mIsAdvertising.postValue(true); } } @Override public void onAdvertisingDataSet(AdvertisingSet advertisingSet, int status) { printLog("onAdvertisingDataSet() :status:" + status); } @Override public void onScanResponseDataSet(AdvertisingSet advertisingSet, int status) { printLog("onScanResponseDataSet(): status:" + status); } @Override public void onAdvertisingSetStopped(AdvertisingSet advertisingSet) { printLog("onAdvertisingSetStopped():"); mIsAdvertising.postValue(false); } }; private void startConnectableAdvertising() { if (mIsAdvertising.getValue()) { return; } BluetoothLeAdvertiser advertiser = mBluetoothAdapter.getBluetoothLeAdvertiser(); AdvertisingSetParameters parameters = new AdvertisingSetParameters.Builder() .setLegacyMode(false) // True by default, but set here as a reminder. .setConnectable(true) .setInterval(AdvertisingSetParameters.INTERVAL_LOW) .setTxPowerLevel(AdvertisingSetParameters.TX_POWER_MEDIUM) .build(); BluetoothGattServerCallback gattServerCallback = new BluetoothGattServerCallback() { @Override public void onConnectionStateChange( BluetoothDevice device, int status, int newState) { super.onConnectionStateChange(device, status, newState); if (newState == BluetoothProfile.STATE_CONNECTED) { printLog("Device connected: " + device.getName()); mTargetDevice.postValue(device); } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { printLog("Device disconnected: " + device.getName()); mTargetDevice.postValue(null); } } }; BluetoothGattServer bluetoothGattServer = mBluetoothManager.openGattServer( getApplication().getApplicationContext(), gattServerCallback); AdvertiseData advertiseData = new AdvertiseData.Builder() .setIncludeDeviceName(true) .addServiceUuid(new ParcelUuid(Constants.CS_TEST_SERVICE_UUID)) .build(); printLog("Start connectable advertising"); advertiser.startAdvertisingSet( parameters, advertiseData, null, null, null, 0, 0, mAdvertisingSetCallback); } private void stopAdvertising() { BluetoothLeAdvertiser advertiser = mBluetoothAdapter.getBluetoothLeAdvertiser(); advertiser.stopAdvertisingSet(mAdvertisingSetCallback); printLog("stop advertising"); } void updateBondedDevices() { List<String> addresses = new ArrayList<>(); Set<BluetoothDevice> bonded_devices = mBluetoothAdapter.getBondedDevices(); for (BluetoothDevice device : bonded_devices) { addresses.add(device.getAddress()); } mBondedBtDeviceAddresses.setValue(addresses); } void setCsTargetAddress(String btAddress) { printLog("set target address: " + btAddress); mTargetBtAddress = btAddress; } void toggleGattConnection() { if (mGattState.getValue() == GattState.DISCONNECTED) { if (TextUtils.isEmpty(mTargetBtAddress)) { printLog("Pair and select a target device first!"); return; } connectGatt(); } else if (mGattState.getValue() == GattState.CONNECTED_DIRECT) { disconnectGatt(); } } private BluetoothGattCallback mGattCallback = new BluetoothGattCallback() { @Override public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { printLog("onConnectionStateChange status:" + status + ", newState:" + newState); if (newState == BluetoothProfile.STATE_CONNECTED) { printLog(gatt.getDevice().getName() + " is connected"); gatt.requestMtu(GATT_MTU_SIZE); mBluetoothGatt = gatt; mGattState.postValue(mExpectedGattState); mTargetDevice.postValue(gatt.getDevice()); } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { printLog("disconnected from " + gatt.getDevice().getName()); mExpectedGattState = GattState.DISCONNECTED; mGattState.postValue(mExpectedGattState); mBluetoothGatt.close(); mBluetoothGatt = null; mTargetDevice.postValue(null); } } public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) { if (status == BluetoothGatt.GATT_SUCCESS) { printLog("MTU changed to: " + mtu); } else { printLog("MTU change failed: " + status); } } }; private void connectGatt() { BluetoothDevice btDevice = mBluetoothAdapter.getRemoteDevice(mTargetBtAddress); printLog("Connect gatt to " + btDevice.getName()); mExpectedGattState = GattState.CONNECTED_DIRECT; btDevice.connectGatt( getApplication().getApplicationContext(), false, mGattCallback, BluetoothDevice.TRANSPORT_LE); } private void disconnectGatt() { if (mBluetoothGatt != null) { printLog("disconnect from " + mBluetoothGatt.getDevice().getName()); mBluetoothGatt.disconnect(); } } void toggleScanConnect() { if (mGattState.getValue() == GattState.DISCONNECTED) { connectGattByScanning(); } else if (mGattState.getValue() == GattState.SCANNING) { stopScanning(); } else if (mGattState.getValue() == GattState.CONNECTED_SCAN) { disconnectGatt(); } } private ScanCallback mScanCallback = new ScanCallback() { @Override public void onScanResult(int callbackType, ScanResult result) { List<ParcelUuid> serviceUuids = result.getScanRecord().getServiceUuids(); if (serviceUuids != null) { for (ParcelUuid parcelUuid : serviceUuids) { BluetoothDevice btDevice = result.getDevice(); printLog("found device - " + btDevice.getName()); if (parcelUuid.getUuid().equals(Constants.CS_TEST_SERVICE_UUID)) { mExpectedGattState = GattState.CONNECTED_SCAN; stopScanning(); printLog("connect GATT to: " + btDevice.getName()); // Connect to the GATT server mBluetoothGatt = btDevice.connectGatt( getApplication().getApplicationContext(), false, mGattCallback, BluetoothDevice.TRANSPORT_LE); } } } } }; private void connectGattByScanning() { BluetoothLeScanner bluetoothLeScanner = mBluetoothAdapter.getBluetoothLeScanner(); List<ScanFilter> filters = new ArrayList<>(); ScanFilter filter = new ScanFilter.Builder() .setServiceUuid( new ParcelUuid( Constants.CS_TEST_SERVICE_UUID)) // Filter by service UUID .build(); filters.add(filter); ScanSettings settings = new ScanSettings.Builder() .setLegacy(false) .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES) .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) .setReportDelay(0) .build(); printLog("start scanning..."); // Start scanning bluetoothLeScanner.startScan(filters, settings, mScanCallback); mExpectedGattState = GattState.SCANNING; mGattState.setValue(mExpectedGattState); } private void stopScanning() { BluetoothLeScanner bluetoothLeScanner = mBluetoothAdapter.getBluetoothLeScanner(); if (bluetoothLeScanner != null) { bluetoothLeScanner.stopScan(mScanCallback); if (mExpectedGattState == GattState.SCANNING) { mExpectedGattState = GattState.DISCONNECTED; mGattState.setValue(mExpectedGattState); } } } private void printLog(@NonNull String logMsg) { mLogText.postValue("BT Log: " + logMsg); } } android/ChannelSoundingTestApp/app/src/main/java/com/android/bluetooth/channelsoundingtestapp/Constants.java 0 → 100644 +31 −0 Original line number Diff line number Diff line /* * Copyright 2024 The Android Open Source Project * * 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 * * http://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.android.bluetooth.channelsoundingtestapp; import java.util.UUID; abstract class Constants { static final UUID CS_TEST_SERVICE_UUID = UUID.fromString("f81d4fae-7ccc-eeee-a765-00aaaaaaaaaa"); enum GattState { DISCONNECTED, SCANNING, CONNECTED_DIRECT, CONNECTED_SCAN, } } Loading
android/ChannelSoundingTestApp/app/build.gradle.kts +6 −3 Original line number Diff line number Diff line Loading @@ -10,8 +10,8 @@ android { applicationId = "com.android.bluetooth.channelsoundingtestapp" minSdk = 34 targetSdk = 34 versionCode = 1 versionName = "1.0" versionCode = 2 versionName = "2.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } Loading @@ -38,6 +38,9 @@ dependencies { implementation(libs.constraintlayout) implementation(libs.navigation.fragment) implementation(libs.navigation.ui) implementation(libs.legacy.support.v4) implementation(libs.lifecycle.livedata.ktx) implementation(libs.lifecycle.viewmodel.ktx) testImplementation(libs.junit) androidTestImplementation(libs.ext.junit) androidTestImplementation(libs.espresso.core) Loading
android/ChannelSoundingTestApp/app/src/main/AndroidManifest.xml +5 −2 Original line number Diff line number Diff line Loading @@ -4,9 +4,12 @@ package="com.android.bluetooth.channelsoundingtestapp"> <uses-feature android:name="android.hardware.bluetooth" android:required="true"/> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" /> <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" /> <uses-permission android:name="android.permission.BLUETOOTH_PRIVILEGED" /> <uses-permission android:name="android.permission.BLUETOOTH_SCAN" /> <application android:allowBackup="true" Loading @@ -17,7 +20,7 @@ android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.ChannelSoundingTestApp" tools:targetApi="31"> tools:targetApi="34"> <activity android:name=".MainActivity" android:exported="true" Loading
android/ChannelSoundingTestApp/app/src/main/java/com/android/bluetooth/channelsoundingtestapp/BleConnectionFragment.java 0 → 100644 +162 −0 Original line number Diff line number Diff line /* * Copyright 2024 The Android Open Source Project * * 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 * * http://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.android.bluetooth.channelsoundingtestapp; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.AdapterView.OnItemSelectedListener; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.Spinner; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import java.util.ArrayList; /** Child fragment to handle BLE GATT connection. */ @SuppressWarnings("SetTextI18n") public class BleConnectionFragment extends Fragment { private BleConnectionViewModel mViewModel; private Button mBtnAdvertising; private ArrayAdapter<String> mBondedBtDevicesArrayAdapter; private Button mButtonUpdate; private Button mButtonGatt; private Button mButtonScanConnect; private Spinner mSpinnerBtAddress; public static BleConnectionFragment newInstance() { return new BleConnectionFragment(); } @Override public View onCreateView( @NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View root = inflater.inflate(R.layout.fragment_ble_connection, container, false); mBtnAdvertising = root.findViewById(R.id.btn_advertising); mButtonUpdate = (Button) root.findViewById(R.id.btn_update_devices); mButtonGatt = (Button) root.findViewById(R.id.btn_connect_gatt); mButtonScanConnect = (Button) root.findViewById(R.id.btn_scan_connect); mSpinnerBtAddress = (Spinner) root.findViewById(R.id.spinner_bt_address); return root; } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); mBondedBtDevicesArrayAdapter = new ArrayAdapter<String>( getContext(), android.R.layout.simple_spinner_item, new ArrayList<>()); mBondedBtDevicesArrayAdapter.setDropDownViewResource( android.R.layout.simple_spinner_dropdown_item); mSpinnerBtAddress.setAdapter(mBondedBtDevicesArrayAdapter); mViewModel = new ViewModelProvider(requireParentFragment()).get(BleConnectionViewModel.class); mViewModel .getGattState() .observe( getActivity(), gattSate -> { switch (gattSate) { case CONNECTED_DIRECT: mButtonGatt.setText("Disconnect Gatt"); break; case SCANNING: mButtonScanConnect.setText("Stop Scan"); break; case CONNECTED_SCAN: mButtonScanConnect.setText("Disconnect Gatt"); break; case DISCONNECTED: default: mButtonGatt.setText("Connect Gatt"); mButtonScanConnect.setText("Scan and Connect"); } }); mButtonUpdate.setOnClickListener( v -> { mViewModel.updateBondedDevices(); }); mViewModel .getBondedBtDeviceAddresses() .observe( getActivity(), deviceList -> { mBondedBtDevicesArrayAdapter.clear(); mBondedBtDevicesArrayAdapter.addAll(deviceList); if (mSpinnerBtAddress.getSelectedItem() != null) { String selectedBtAddress = mSpinnerBtAddress.getSelectedItem().toString(); mViewModel.setCsTargetAddress(selectedBtAddress); } }); mSpinnerBtAddress.setOnItemSelectedListener( new OnItemSelectedListener() { @Override public void onItemSelected( AdapterView<?> adapterView, View view, int i, long l) { String btAddress = mSpinnerBtAddress.getSelectedItem().toString(); mViewModel.setCsTargetAddress(btAddress); } @Override public void onNothingSelected(AdapterView<?> adapterView) { mViewModel.setCsTargetAddress(""); } }); mButtonGatt.setOnClickListener( v -> { mViewModel.toggleGattConnection(); }); mButtonScanConnect.setOnClickListener( v -> { mViewModel.toggleScanConnect(); }); mViewModel .getIsAdvertising() .observe( getActivity(), isAdvertising -> { if (isAdvertising) { mBtnAdvertising.setText("Stop Advertising"); } else { mBtnAdvertising.setText("Start Advertising"); } }); mBtnAdvertising.setOnClickListener( new OnClickListener() { @Override public void onClick(View view) { mViewModel.toggleAdvertising(); } }); } }
android/ChannelSoundingTestApp/app/src/main/java/com/android/bluetooth/channelsoundingtestapp/BleConnectionViewModel.java 0 → 100644 +342 −0 Original line number Diff line number Diff line /* * Copyright 2024 The Android Open Source Project * * 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 * * http://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.android.bluetooth.channelsoundingtestapp; import android.annotation.SuppressLint; import android.app.Application; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCallback; import android.bluetooth.BluetoothGattServer; import android.bluetooth.BluetoothGattServerCallback; import android.bluetooth.BluetoothManager; import android.bluetooth.BluetoothProfile; import android.bluetooth.le.AdvertiseData; import android.bluetooth.le.AdvertisingSet; import android.bluetooth.le.AdvertisingSetCallback; import android.bluetooth.le.AdvertisingSetParameters; import android.bluetooth.le.BluetoothLeAdvertiser; import android.bluetooth.le.BluetoothLeScanner; import android.bluetooth.le.ScanCallback; import android.bluetooth.le.ScanFilter; import android.bluetooth.le.ScanResult; import android.bluetooth.le.ScanSettings; import android.os.ParcelUuid; import android.text.TextUtils; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import com.android.bluetooth.channelsoundingtestapp.Constants.GattState; import java.util.ArrayList; import java.util.List; import java.util.Set; /** The ViewModel for the BLE GATT connection. */ @SuppressLint("MissingPermission") // permissions are checked upfront public class BleConnectionViewModel extends AndroidViewModel { private static final int GATT_MTU_SIZE = 512; private final BluetoothAdapter mBluetoothAdapter; private final BluetoothManager mBluetoothManager; @Nullable private BluetoothGatt mBluetoothGatt = null; private MutableLiveData<Boolean> mIsAdvertising = new MutableLiveData<>(false); private MutableLiveData<String> mLogText = new MutableLiveData<>(); private MutableLiveData<BluetoothDevice> mTargetDevice = new MutableLiveData<>(); // scanner private final MutableLiveData<List<String>> mBondedBtDeviceAddresses = new MutableLiveData<>(); private final MutableLiveData<GattState> mGattState = new MutableLiveData<>(GattState.DISCONNECTED); private String mTargetBtAddress = ""; private GattState mExpectedGattState = GattState.DISCONNECTED; /** Constructor */ public BleConnectionViewModel(@NonNull Application application) { super(application); mBluetoothManager = application.getSystemService(BluetoothManager.class); mBluetoothAdapter = mBluetoothManager.getAdapter(); } LiveData<Boolean> getIsAdvertising() { return mIsAdvertising; } LiveData<String> getLogText() { return mLogText; } LiveData<GattState> getGattState() { return mGattState; } LiveData<List<String>> getBondedBtDeviceAddresses() { return mBondedBtDeviceAddresses; } LiveData<BluetoothDevice> getTargetDevice() { return mTargetDevice; } void toggleAdvertising() { if (mIsAdvertising.getValue()) { stopAdvertising(); } else { startConnectableAdvertising(); } } AdvertisingSetCallback mAdvertisingSetCallback = new AdvertisingSetCallback() { @Override public void onAdvertisingSetStarted( AdvertisingSet advertisingSet, int txPower, int status) { printLog( "onAdvertisingSetStarted(): txPower:" + txPower + " , status: " + status); if (status == 0) { mIsAdvertising.postValue(true); } } @Override public void onAdvertisingDataSet(AdvertisingSet advertisingSet, int status) { printLog("onAdvertisingDataSet() :status:" + status); } @Override public void onScanResponseDataSet(AdvertisingSet advertisingSet, int status) { printLog("onScanResponseDataSet(): status:" + status); } @Override public void onAdvertisingSetStopped(AdvertisingSet advertisingSet) { printLog("onAdvertisingSetStopped():"); mIsAdvertising.postValue(false); } }; private void startConnectableAdvertising() { if (mIsAdvertising.getValue()) { return; } BluetoothLeAdvertiser advertiser = mBluetoothAdapter.getBluetoothLeAdvertiser(); AdvertisingSetParameters parameters = new AdvertisingSetParameters.Builder() .setLegacyMode(false) // True by default, but set here as a reminder. .setConnectable(true) .setInterval(AdvertisingSetParameters.INTERVAL_LOW) .setTxPowerLevel(AdvertisingSetParameters.TX_POWER_MEDIUM) .build(); BluetoothGattServerCallback gattServerCallback = new BluetoothGattServerCallback() { @Override public void onConnectionStateChange( BluetoothDevice device, int status, int newState) { super.onConnectionStateChange(device, status, newState); if (newState == BluetoothProfile.STATE_CONNECTED) { printLog("Device connected: " + device.getName()); mTargetDevice.postValue(device); } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { printLog("Device disconnected: " + device.getName()); mTargetDevice.postValue(null); } } }; BluetoothGattServer bluetoothGattServer = mBluetoothManager.openGattServer( getApplication().getApplicationContext(), gattServerCallback); AdvertiseData advertiseData = new AdvertiseData.Builder() .setIncludeDeviceName(true) .addServiceUuid(new ParcelUuid(Constants.CS_TEST_SERVICE_UUID)) .build(); printLog("Start connectable advertising"); advertiser.startAdvertisingSet( parameters, advertiseData, null, null, null, 0, 0, mAdvertisingSetCallback); } private void stopAdvertising() { BluetoothLeAdvertiser advertiser = mBluetoothAdapter.getBluetoothLeAdvertiser(); advertiser.stopAdvertisingSet(mAdvertisingSetCallback); printLog("stop advertising"); } void updateBondedDevices() { List<String> addresses = new ArrayList<>(); Set<BluetoothDevice> bonded_devices = mBluetoothAdapter.getBondedDevices(); for (BluetoothDevice device : bonded_devices) { addresses.add(device.getAddress()); } mBondedBtDeviceAddresses.setValue(addresses); } void setCsTargetAddress(String btAddress) { printLog("set target address: " + btAddress); mTargetBtAddress = btAddress; } void toggleGattConnection() { if (mGattState.getValue() == GattState.DISCONNECTED) { if (TextUtils.isEmpty(mTargetBtAddress)) { printLog("Pair and select a target device first!"); return; } connectGatt(); } else if (mGattState.getValue() == GattState.CONNECTED_DIRECT) { disconnectGatt(); } } private BluetoothGattCallback mGattCallback = new BluetoothGattCallback() { @Override public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { printLog("onConnectionStateChange status:" + status + ", newState:" + newState); if (newState == BluetoothProfile.STATE_CONNECTED) { printLog(gatt.getDevice().getName() + " is connected"); gatt.requestMtu(GATT_MTU_SIZE); mBluetoothGatt = gatt; mGattState.postValue(mExpectedGattState); mTargetDevice.postValue(gatt.getDevice()); } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { printLog("disconnected from " + gatt.getDevice().getName()); mExpectedGattState = GattState.DISCONNECTED; mGattState.postValue(mExpectedGattState); mBluetoothGatt.close(); mBluetoothGatt = null; mTargetDevice.postValue(null); } } public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) { if (status == BluetoothGatt.GATT_SUCCESS) { printLog("MTU changed to: " + mtu); } else { printLog("MTU change failed: " + status); } } }; private void connectGatt() { BluetoothDevice btDevice = mBluetoothAdapter.getRemoteDevice(mTargetBtAddress); printLog("Connect gatt to " + btDevice.getName()); mExpectedGattState = GattState.CONNECTED_DIRECT; btDevice.connectGatt( getApplication().getApplicationContext(), false, mGattCallback, BluetoothDevice.TRANSPORT_LE); } private void disconnectGatt() { if (mBluetoothGatt != null) { printLog("disconnect from " + mBluetoothGatt.getDevice().getName()); mBluetoothGatt.disconnect(); } } void toggleScanConnect() { if (mGattState.getValue() == GattState.DISCONNECTED) { connectGattByScanning(); } else if (mGattState.getValue() == GattState.SCANNING) { stopScanning(); } else if (mGattState.getValue() == GattState.CONNECTED_SCAN) { disconnectGatt(); } } private ScanCallback mScanCallback = new ScanCallback() { @Override public void onScanResult(int callbackType, ScanResult result) { List<ParcelUuid> serviceUuids = result.getScanRecord().getServiceUuids(); if (serviceUuids != null) { for (ParcelUuid parcelUuid : serviceUuids) { BluetoothDevice btDevice = result.getDevice(); printLog("found device - " + btDevice.getName()); if (parcelUuid.getUuid().equals(Constants.CS_TEST_SERVICE_UUID)) { mExpectedGattState = GattState.CONNECTED_SCAN; stopScanning(); printLog("connect GATT to: " + btDevice.getName()); // Connect to the GATT server mBluetoothGatt = btDevice.connectGatt( getApplication().getApplicationContext(), false, mGattCallback, BluetoothDevice.TRANSPORT_LE); } } } } }; private void connectGattByScanning() { BluetoothLeScanner bluetoothLeScanner = mBluetoothAdapter.getBluetoothLeScanner(); List<ScanFilter> filters = new ArrayList<>(); ScanFilter filter = new ScanFilter.Builder() .setServiceUuid( new ParcelUuid( Constants.CS_TEST_SERVICE_UUID)) // Filter by service UUID .build(); filters.add(filter); ScanSettings settings = new ScanSettings.Builder() .setLegacy(false) .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES) .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) .setReportDelay(0) .build(); printLog("start scanning..."); // Start scanning bluetoothLeScanner.startScan(filters, settings, mScanCallback); mExpectedGattState = GattState.SCANNING; mGattState.setValue(mExpectedGattState); } private void stopScanning() { BluetoothLeScanner bluetoothLeScanner = mBluetoothAdapter.getBluetoothLeScanner(); if (bluetoothLeScanner != null) { bluetoothLeScanner.stopScan(mScanCallback); if (mExpectedGattState == GattState.SCANNING) { mExpectedGattState = GattState.DISCONNECTED; mGattState.setValue(mExpectedGattState); } } } private void printLog(@NonNull String logMsg) { mLogText.postValue("BT Log: " + logMsg); } }
android/ChannelSoundingTestApp/app/src/main/java/com/android/bluetooth/channelsoundingtestapp/Constants.java 0 → 100644 +31 −0 Original line number Diff line number Diff line /* * Copyright 2024 The Android Open Source Project * * 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 * * http://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.android.bluetooth.channelsoundingtestapp; import java.util.UUID; abstract class Constants { static final UUID CS_TEST_SERVICE_UUID = UUID.fromString("f81d4fae-7ccc-eeee-a765-00aaaaaaaaaa"); enum GattState { DISCONNECTED, SCANNING, CONNECTED_DIRECT, CONNECTED_SCAN, } }