Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit c5e98891 authored by Android Build Coastguard Worker's avatar Android Build Coastguard Worker
Browse files

Snap for 12313714 from 707f9358 to 24Q4-release

Change-Id: If35effa84a20772978836ca2af9bef2d6c3b97a7
parents 43cffd47 707f9358
Loading
Loading
Loading
Loading
+6 −3
Original line number Diff line number Diff line
@@ -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"
  }
@@ -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)
+5 −2
Original line number Diff line number Diff line
@@ -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"
@@ -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"
+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();
                    }
                });
    }
}
+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);
    }
}
+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