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

Commit f9b338a7 authored by Jack He's avatar Jack He Committed by Android (Google) Code Review
Browse files

Merge changes Ib2b4c332,Ief24ac1d,Ic2909d05,I6e68ddb1,Ia1d38777 into tm-qpr-dev

* changes:
  Bass: Multiple improvements and minor fixes
  bass: Add BassClientStateMachine unit tests
  BassClient: Verify connection state change intents
  BassClient: State machine cleanup
  BassClient: Add initial support for group operations
parents 168c5ccc 1df50f15
Loading
Loading
Loading
Loading
+456 −85

File changed.

Preview size limit exceeded, changes collapsed.

+45 −27
Original line number Diff line number Diff line
@@ -98,8 +98,6 @@ import java.io.ByteArrayOutputStream;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@@ -138,6 +136,10 @@ public class BassClientStateMachine extends StateMachine {
    static final int PSYNC_ACTIVE_TIMEOUT = 14;
    static final int CONNECT_TIMEOUT = 15;

    // NOTE: the value is not "final" - it is modified in the unit tests
    @VisibleForTesting
    private int mConnectTimeoutMs;

    /*key is combination of sourceId, Address and advSid for this hashmap*/
    private final Map<Integer, BluetoothLeBroadcastReceiveState>
            mBluetoothLeBroadcastReceiveStates =
@@ -150,14 +152,15 @@ public class BassClientStateMachine extends StateMachine {
    private final ConnectedProcessing mConnectedProcessing = new ConnectedProcessing();
    private final List<BluetoothGattCharacteristic> mBroadcastCharacteristics =
            new ArrayList<BluetoothGattCharacteristic>();
    private final BluetoothDevice mDevice;
    @VisibleForTesting
    BluetoothDevice mDevice;

    private boolean mIsAllowedList = false;
    private int mLastConnectionState = -1;
    private boolean mMTUChangeRequested = false;
    private boolean mDiscoveryInitiated = false;
    private BassClientService mService;
    private BluetoothGatt mBluetoothGatt = null;
    @VisibleForTesting
    BassClientService mService;

    private BluetoothGattCharacteristic mBroadcastScanControlPoint;
    private boolean mFirstTimeBisDiscovery = false;
@@ -183,10 +186,15 @@ public class BassClientStateMachine extends StateMachine {
    private int mBroadcastSourceIdLength = 3;
    private byte mNextSourceId = 0;

    BassClientStateMachine(BluetoothDevice device, BassClientService svc, Looper looper) {
    BluetoothGatt mBluetoothGatt = null;
    BluetoothGattCallback mGattCallback = null;

    BassClientStateMachine(BluetoothDevice device, BassClientService svc, Looper looper,
            int connectTimeoutMs) {
        super(TAG + "(" + device.toString() + ")", looper);
        mDevice = device;
        mService = svc;
        mConnectTimeoutMs = connectTimeoutMs;
        addState(mDisconnected);
        addState(mDisconnecting);
        addState(mConnected);
@@ -211,7 +219,8 @@ public class BassClientStateMachine extends StateMachine {
    static BassClientStateMachine make(BluetoothDevice device,
            BassClientService svc, Looper looper) {
        Log.d(TAG, "make for device " + device);
        BassClientStateMachine BassclientSm = new BassClientStateMachine(device, svc, looper);
        BassClientStateMachine BassclientSm = new BassClientStateMachine(device, svc, looper,
                BassConstants.CONNECT_TIMEOUT_MS);
        BassclientSm.start();
        return BassclientSm;
    }
@@ -240,6 +249,7 @@ public class BassClientStateMachine extends StateMachine {
            mBluetoothGatt.disconnect();
            mBluetoothGatt.close();
            mBluetoothGatt = null;
            mGattCallback = null;
        }
        mPendingOperation = -1;
        mPendingSourceId = -1;
@@ -785,8 +795,8 @@ public class BassClientStateMachine extends StateMachine {
            if (oldRecvState.getSourceDevice() == null
                    || oldRecvState.getSourceDevice().getAddress().equals(emptyBluetoothDevice)) {
                log("New Source Addition");
                mService.getCallbacks().notifySourceAdded(mDevice,
                        recvState.getSourceId(), BluetoothStatusCodes.REASON_LOCAL_APP_REQUEST);
                mService.getCallbacks().notifySourceAdded(mDevice, recvState,
                        BluetoothStatusCodes.REASON_LOCAL_APP_REQUEST);
                if (mPendingMetadata != null) {
                    setCurrentBroadcastMetadata(recvState.getSourceId(), mPendingMetadata);
                }
@@ -823,8 +833,7 @@ public class BassClientStateMachine extends StateMachine {

    // Implements callback methods for GATT events that the app cares about.
    // For example, connection change and services discovered.
    private final BluetoothGattCallback mGattCallback =
            new BluetoothGattCallback() {
    final class GattCallback extends BluetoothGattCallback {
                @Override
                public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
                    boolean isStateChanged = false;
@@ -977,6 +986,25 @@ public class BassClientStateMachine extends StateMachine {
                }
            };

    /**
     * Connects to the GATT server of the device.
     *
     * @return {@code true} if it successfully connects to the GATT server.
     */
    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
    public boolean connectGatt(Boolean autoConnect) {
        if (mGattCallback == null) {
            mGattCallback = new GattCallback();
        }

        mBluetoothGatt = mDevice.connectGatt(mService, autoConnect,
                mGattCallback, BluetoothDevice.TRANSPORT_LE,
                (BluetoothDevice.PHY_LE_1M_MASK
                        | BluetoothDevice.PHY_LE_2M_MASK
                        | BluetoothDevice.PHY_LE_CODED_MASK), null);
        return mBluetoothGatt != null;
    }

    /**
     * getAllSources
     */
@@ -1050,11 +1078,7 @@ public class BassClientStateMachine extends StateMachine {
                if (mLastConnectionState != BluetoothProfile.STATE_DISCONNECTED) {
                    // Reconnect in background if not disallowed by the service
                    if (mService.okToConnect(mDevice)) {
                        mBluetoothGatt = mDevice.connectGatt(mService, true,
                                mGattCallback, BluetoothDevice.TRANSPORT_LE,
                                (BluetoothDevice.PHY_LE_1M_MASK
                                        | BluetoothDevice.PHY_LE_2M_MASK
                                        | BluetoothDevice.PHY_LE_CODED_MASK), null);
                        connectGatt(false);
                    }
                }
            }
@@ -1080,16 +1104,10 @@ public class BassClientStateMachine extends StateMachine {
                        mBluetoothGatt.close();
                        mBluetoothGatt = null;
                    }
                    mBluetoothGatt = mDevice.connectGatt(mService, mIsAllowedList,
                            mGattCallback, BluetoothDevice.TRANSPORT_LE, false,
                            (BluetoothDevice.PHY_LE_1M_MASK
                                    | BluetoothDevice.PHY_LE_2M_MASK
                                    | BluetoothDevice.PHY_LE_CODED_MASK), null);
                    if (mBluetoothGatt == null) {
                        Log.e(TAG, "Disconnected: error connecting to " + mDevice);
                        break;
                    } else {
                    if (connectGatt(mIsAllowedList)) {
                        transitionTo(mConnecting);
                    } else {
                        Log.e(TAG, "Disconnected: error connecting to " + mDevice);
                    }
                    break;
                case DISCONNECT:
@@ -1130,7 +1148,7 @@ public class BassClientStateMachine extends StateMachine {
        public void enter() {
            log("Enter Connecting(" + mDevice + "): "
                    + messageWhatToString(getCurrentMessage().what));
            sendMessageDelayed(CONNECT_TIMEOUT, mDevice, BassConstants.CONNECT_TIMEOUT_MS);
            sendMessageDelayed(CONNECT_TIMEOUT, mDevice, mConnectTimeoutMs);
            broadcastConnectionState(
                    mDevice, mLastConnectionState, BluetoothProfile.STATE_CONNECTING);
        }
@@ -1786,7 +1804,7 @@ public class BassClientStateMachine extends StateMachine {
        public void enter() {
            log("Enter Disconnecting(" + mDevice + "): "
                    + messageWhatToString(getCurrentMessage().what));
            sendMessageDelayed(CONNECT_TIMEOUT, mDevice, BassConstants.CONNECT_TIMEOUT_MS);
            sendMessageDelayed(CONNECT_TIMEOUT, mDevice, mConnectTimeoutMs);
            broadcastConnectionState(
                    mDevice, mLastConnectionState, BluetoothProfile.STATE_DISCONNECTING);
        }
+21 −2
Original line number Diff line number Diff line
@@ -52,6 +52,7 @@ import com.android.internal.annotations.VisibleForTesting;
import com.android.modules.utils.SynchronousResultReceiver;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -585,7 +586,7 @@ public class CsipSetCoordinatorService extends ProfileService {

    /**
     * Get collection of group IDs for a given UUID
     * @param uuid
     * @param uuid profile context UUID
     * @return list of group IDs
     */
    public List<Integer> getAllGroupIds(ParcelUuid uuid) {
@@ -598,7 +599,7 @@ public class CsipSetCoordinatorService extends ProfileService {

    /**
     * Get device's groups/
     * @param device
     * @param device group member device
     * @return map of group id and related uuids.
     */
    public Map<Integer, ParcelUuid> getGroupUuidMapByDevice(BluetoothDevice device) {
@@ -633,6 +634,24 @@ public class CsipSetCoordinatorService extends ProfileService {
                .collect(Collectors.toList());
    }

    /**
     * Get grouped devices
     * @param device group member device
     * @param uuid profile context UUID
     * @return related list of devices sorted from the lowest to the highest rank value.
     */
    public @NonNull List<BluetoothDevice> getGroupDevicesOrdered(BluetoothDevice device,
            ParcelUuid uuid) {
        List<Integer> groupIds = getAllGroupIds(uuid);
        for (Integer id : groupIds) {
            List<BluetoothDevice> devices = getGroupDevicesOrdered(id);
            if (devices.contains(device)) {
                return devices;
            }
        }
        return Collections.emptyList();
    }

    /**
     * Get group desired size
     * @param groupId group ID
+757 −1

File changed.

Preview size limit exceeded, changes collapsed.

+245 −0
Original line number Diff line number Diff line
/*
 * Copyright 2022 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.bass_client;

import static android.bluetooth.BluetoothGatt.GATT_SUCCESS;

import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.after;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.reset;

import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.HandlerThread;
import android.os.Looper;

import androidx.test.filters.MediumTest;

import com.android.bluetooth.R;
import com.android.bluetooth.TestUtils;
import com.android.bluetooth.btservice.AdapterService;

import org.hamcrest.core.IsInstanceOf;
import org.junit.After;
import org.junit.Assert;
import org.junit.Assume;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;

@MediumTest
@RunWith(JUnit4.class)
public class BassClientStateMachineTest {
    @Rule
    public final MockitoRule mockito = MockitoJUnit.rule();

    private BluetoothAdapter mAdapter;
    private HandlerThread mHandlerThread;
    private StubBassClientStateMachine mBassClientStateMachine;
    private static final int CONNECTION_TIMEOUT_MS = 1_000;
    private static final int TIMEOUT_MS = 2_000;
    private static final int WAIT_MS = 1_200;

    private BluetoothDevice mTestDevice;
    @Mock private AdapterService mAdapterService;
    @Mock private BassClientService mBassClientService;

    @Before
    public void setUp() throws Exception {
        TestUtils.setAdapterService(mAdapterService);

        mAdapter = BluetoothAdapter.getDefaultAdapter();

        // Get a device for testing
        mTestDevice = mAdapter.getRemoteDevice("00:01:02:03:04:05");

        // Set up thread and looper
        mHandlerThread = new HandlerThread("BassClientStateMachineTestHandlerThread");
        mHandlerThread.start();
        mBassClientStateMachine = new StubBassClientStateMachine(mTestDevice,
                mBassClientService, mHandlerThread.getLooper(), CONNECTION_TIMEOUT_MS);
        mBassClientStateMachine.start();
    }

    @After
    public void tearDown() throws Exception {
        mBassClientStateMachine.doQuit();
        mHandlerThread.quit();
        TestUtils.clearAdapterService(mAdapterService);
    }

    /**
     * Test that default state is disconnected
     */
    @Test
    public void testDefaultDisconnectedState() {
        Assert.assertEquals(BluetoothProfile.STATE_DISCONNECTED,
                mBassClientStateMachine.getConnectionState());
    }

    /**
     * Allow/disallow connection to any device.
     *
     * @param allow if true, connection is allowed
     */
    private void allowConnection(boolean allow) {
        when(mBassClientService.okToConnect(any(BluetoothDevice.class))).thenReturn(allow);
    }

    private void allowConnectGatt(boolean allow) {
        mBassClientStateMachine.mShouldAllowGatt = allow;
    }

    /**
     * Test that an incoming connection with policy forbidding connection is rejected
     */
    @Test
    public void testOkToConnectFails() {
        allowConnection(false);
        allowConnectGatt(true);

        // Inject an event for when incoming connection is requested
        mBassClientStateMachine.sendMessage(BassClientStateMachine.CONNECT);

        // Verify that no connection state broadcast is executed
        verify(mBassClientService, after(WAIT_MS).never()).sendBroadcast(any(Intent.class),
                anyString());

        // Check that we are in Disconnected state
        Assert.assertThat(mBassClientStateMachine.getCurrentState(),
                IsInstanceOf.instanceOf(BassClientStateMachine.Disconnected.class));
    }

    @Test
    public void testFailToConnectGatt() {
        allowConnection(true);
        allowConnectGatt(false);

        // Inject an event for when incoming connection is requested
        mBassClientStateMachine.sendMessage(BassClientStateMachine.CONNECT);

        // Verify that no connection state broadcast is executed
        verify(mBassClientService, after(WAIT_MS).never()).sendBroadcast(any(Intent.class),
                anyString());

        // Check that we are in Disconnected state
        Assert.assertThat(mBassClientStateMachine.getCurrentState(),
                IsInstanceOf.instanceOf(BassClientStateMachine.Disconnected.class));
        assertNull(mBassClientStateMachine.mBluetoothGatt);
    }

    @Test
    public void testSuccessfullyConnected() {
        allowConnection(true);
        allowConnectGatt(true);

        // Inject an event for when incoming connection is requested
        mBassClientStateMachine.sendMessage(BassClientStateMachine.CONNECT);

        // Verify that one connection state broadcast is executed
        ArgumentCaptor<Intent> intentArgument1 = ArgumentCaptor.forClass(Intent.class);
        verify(mBassClientService, timeout(TIMEOUT_MS).times(1)).sendBroadcast(
                intentArgument1.capture(), anyString(), any(Bundle.class));
        Assert.assertEquals(BluetoothProfile.STATE_CONNECTING,
                intentArgument1.getValue().getIntExtra(BluetoothProfile.EXTRA_STATE, -1));

        Assert.assertThat(mBassClientStateMachine.getCurrentState(),
                IsInstanceOf.instanceOf(BassClientStateMachine.Connecting.class));

        assertNotNull(mBassClientStateMachine.mGattCallback);
        mBassClientStateMachine.notifyConnectionStateChanged(
                GATT_SUCCESS, BluetoothProfile.STATE_CONNECTED);

        // Verify that the expected number of broadcasts are executed:
        // - two calls to broadcastConnectionState(): Disconnected -> Connecting -> Connected
        ArgumentCaptor<Intent> intentArgument2 = ArgumentCaptor.forClass(Intent.class);
        verify(mBassClientService, timeout(TIMEOUT_MS).times(2)).sendBroadcast(
                intentArgument2.capture(), anyString(), any(Bundle.class));

        Assert.assertThat(mBassClientStateMachine.getCurrentState(),
                IsInstanceOf.instanceOf(BassClientStateMachine.Connected.class));
    }

    @Test
    public void testConnectGattTimeout() {
        allowConnection(true);
        allowConnectGatt(true);

        // Inject an event for when incoming connection is requested
        mBassClientStateMachine.sendMessage(BassClientStateMachine.CONNECT);

        // Verify that one connection state broadcast is executed
        ArgumentCaptor<Intent> intentArgument1 = ArgumentCaptor.forClass(Intent.class);
        verify(mBassClientService, timeout(TIMEOUT_MS).times(1)).sendBroadcast(
                intentArgument1.capture(), anyString(), any(Bundle.class));
        Assert.assertEquals(BluetoothProfile.STATE_CONNECTING,
                intentArgument1.getValue().getIntExtra(BluetoothProfile.EXTRA_STATE, -1));

        Assert.assertThat(mBassClientStateMachine.getCurrentState(),
                IsInstanceOf.instanceOf(BassClientStateMachine.Connecting.class));

        // Verify that one connection state broadcast is executed
        ArgumentCaptor<Intent> intentArgument2 = ArgumentCaptor.forClass(Intent.class);
        verify(mBassClientService, timeout(TIMEOUT_MS).times(
                2)).sendBroadcast(intentArgument2.capture(), anyString(), any(Bundle.class));
        Assert.assertEquals(BluetoothProfile.STATE_DISCONNECTED,
                intentArgument2.getValue().getIntExtra(BluetoothProfile.EXTRA_STATE, -1));

        Assert.assertThat(mBassClientStateMachine.getCurrentState(),
                IsInstanceOf.instanceOf(BassClientStateMachine.Disconnected.class));
    }

    // It simulates GATT connection for testing.
    public static class StubBassClientStateMachine extends BassClientStateMachine {
        boolean mShouldAllowGatt = true;

        StubBassClientStateMachine(BluetoothDevice device, BassClientService service, Looper looper,
                int connectTimeout) {
            super(device, service, looper, connectTimeout);
        }

        @Override
        public boolean connectGatt(Boolean autoConnect) {
            mGattCallback = new GattCallback();
            return mShouldAllowGatt;
        }

        public void notifyConnectionStateChanged(int status, int newState) {
            if (mGattCallback != null) {
                mGattCallback.onConnectionStateChange(mBluetoothGatt, status, newState);
            }
        }
    }
}