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

Commit 6d2900cf authored by Jakub Pawłowski's avatar Jakub Pawłowski Committed by Gerrit Code Review
Browse files

Merge "LeAudio: Respond to TMAP read requests"

parents ba9fca76 97d8ebdc
Loading
Loading
Loading
Loading
+73 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 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.le_audio;

import android.content.Context;
import android.util.Log;

import com.android.bluetooth.Utils;
import com.android.internal.annotations.VisibleForTesting;

/**
 * Factory class for object initialization to help with unit testing
 */
public class LeAudioObjectsFactory {
    private static final String TAG = LeAudioObjectsFactory.class.getSimpleName();
    private static LeAudioObjectsFactory sInstance;
    private static final Object INSTANCE_LOCK = new Object();

    private LeAudioObjectsFactory() {}

    /**
     * Get the singleton instance of object factory
     *
     * @return the singleton instance, guaranteed not null
     */
    public static LeAudioObjectsFactory getInstance() {
        synchronized (INSTANCE_LOCK) {
            if (sInstance == null) {
                sInstance = new LeAudioObjectsFactory();
            }
        }
        return sInstance;
    }

    /**
     * Allow unit tests to substitute {@link LeAudioObjectsFactory} with a test instance
     *
     * @param objectsFactory a test instance of the {@link LeAudioObjectsFactory}
     */
    @VisibleForTesting
    public static void setInstanceForTesting(LeAudioObjectsFactory objectsFactory) {
        Utils.enforceInstrumentationTestMode();
        synchronized (INSTANCE_LOCK) {
            Log.d(TAG, "setInstanceForTesting(), set to " + objectsFactory);
            sInstance = objectsFactory;
        }
    }

    /**
     * Get a {@link LeAudioTmapGattServer} object
     *
     * @param context local context
     * @return
     */
    public LeAudioTmapGattServer getTmapGattServer(Context context) {
        return new LeAudioTmapGattServer(
                new LeAudioTmapGattServer.BluetoothGattServerProxy(context));
    }
}
+21 −0
Original line number Diff line number Diff line
@@ -121,6 +121,7 @@ public class LeAudioService extends ProfileService {
    LeAudioBroadcasterNativeInterface mLeAudioBroadcasterNativeInterface = null;
    @VisibleForTesting
    AudioManager mAudioManager;
    LeAudioTmapGattServer mTmapGattServer;

    @VisibleForTesting
    RemoteCallbackList<IBluetoothLeBroadcastCallback> mBroadcastCallbacks;
@@ -236,6 +237,9 @@ public class LeAudioService extends ProfileService {
        registerReceiver(mConnectionStateChangedReceiver, filter);
        mLeAudioCallbacks = new RemoteCallbackList<IBluetoothLeAudioCallback>();

        int tmapRoleMask =
                LeAudioTmapGattServer.TMAP_ROLE_FLAG_CG | LeAudioTmapGattServer.TMAP_ROLE_FLAG_UMS;

        // Initialize Broadcast native interface
        if (mAdapterService.isLeAudioBroadcastSourceSupported()) {
            mBroadcastCallbacks = new RemoteCallbackList<IBluetoothLeBroadcastCallback>();
@@ -243,9 +247,18 @@ public class LeAudioService extends ProfileService {
                    LeAudioBroadcasterNativeInterface.getInstance(),
                    "LeAudioBroadcasterNativeInterface cannot be null when LeAudioService starts");
            mLeAudioBroadcasterNativeInterface.init();
            tmapRoleMask |= LeAudioTmapGattServer.TMAP_ROLE_FLAG_BMS;
        } else {
            Log.w(TAG, "Le Audio Broadcasts not supported.");
        }

        // the role mask is fixed in Android
        if (mTmapGattServer != null) {
            throw new IllegalStateException("TMAP GATT server started before start() is called");
        }
        mTmapGattServer = LeAudioObjectsFactory.getInstance().getTmapGattServer(this);
        mTmapGattServer.start(tmapRoleMask);

        // Mark service as started
        setLeAudioService(this);

@@ -276,6 +289,14 @@ public class LeAudioService extends ProfileService {
        }

        setActiveDevice(null);

        if (mTmapGattServer == null) {
            Log.w(TAG, "TMAP GATT server should never be null before stop() is called");
        } else {
            mTmapGattServer.stop();
            mTmapGattServer = null;
        }

        //Don't wait for async call with INACTIVE group status, clean active
        //device for active group.
        synchronized (mGroupLock) {
+210 −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.le_audio;

import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattServer;
import android.bluetooth.BluetoothGattServerCallback;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothUuid;
import android.content.Context;
import android.util.Log;

import com.android.internal.annotations.VisibleForTesting;

import java.util.Arrays;
import java.util.List;
import java.util.UUID;

/**
 * A GATT server for Telephony and Media Audio Profile (TMAP)
 */
@VisibleForTesting
public class LeAudioTmapGattServer {
    private static final boolean DBG = true;
    private static final String TAG = "LeAudioTmapGattServer";

    /* Telephony and Media Audio Profile Role Characteristic UUID */
    @VisibleForTesting
    public static final UUID UUID_TMAP_ROLE =
            UUID.fromString("00002B51-0000-1000-8000-00805f9b34fb");

    /* TMAP Role: Call Gateway */
    public static final int TMAP_ROLE_FLAG_CG = 1;
    /* TMAP Role: Call Terminal */
    public static final int TMAP_ROLE_FLAG_CT = 1 << 1;
    /* TMAP Role: Unicast Media Sender */
    public static final int TMAP_ROLE_FLAG_UMS = 1 << 2;
    /* TMAP Role: Unicast Media Receiver */
    public static final int TMAP_ROLE_FLAG_UMR = 1 << 3;
    /* TMAP Role: Broadcast Media Sender */
    public static final int TMAP_ROLE_FLAG_BMS = 1 << 4;
    /* TMAP Role: Broadcast Media Receiver */
    public static final int TMAP_ROLE_FLAG_BMR = 1 << 5;

    private final BluetoothGattServerProxy mBluetoothGattServer;

    /*package*/ LeAudioTmapGattServer(BluetoothGattServerProxy gattServer) {
        mBluetoothGattServer = gattServer;
    }

    /**
     * Init TMAP server
     * @param roleMask bit mask of supported roles.
     */
    @VisibleForTesting
    public void start(int roleMask) {
        if (DBG) {
            Log.d(TAG, "start(roleMask:" + roleMask + ")");
        }

        if (!mBluetoothGattServer.open(mBluetoothGattServerCallback)) {
            throw new IllegalStateException("Could not open Gatt server");
        }

        BluetoothGattService service =
                new BluetoothGattService(BluetoothUuid.TMAP.getUuid(),
                BluetoothGattService.SERVICE_TYPE_PRIMARY);

        BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(
                UUID_TMAP_ROLE,
                BluetoothGattCharacteristic.PROPERTY_READ,
                BluetoothGattCharacteristic.PERMISSION_READ_ENCRYPTED);

        characteristic.setValue(roleMask, BluetoothGattCharacteristic.FORMAT_UINT16, 0);
        service.addCharacteristic(characteristic);

        if (!mBluetoothGattServer.addService(service)) {
            throw new IllegalStateException("Failed to add service for TMAP");
        }
    }

    /**
     * Stop TMAP server
     */
    @VisibleForTesting
    public void stop() {
        if (DBG) {
            Log.d(TAG, "stop()");
        }
        if (mBluetoothGattServer == null) {
            Log.w(TAG, "mBluetoothGattServer should not be null when stop() is called");
            return;
        }
        mBluetoothGattServer.close();
    }

    /**
     * Callback to handle incoming requests to the GATT server.
     * All read/write requests for characteristics and descriptors are handled here.
     */
    private final BluetoothGattServerCallback mBluetoothGattServerCallback =
            new BluetoothGattServerCallback() {
        @Override
        public void onCharacteristicReadRequest(BluetoothDevice device, int requestId, int offset,
                                                BluetoothGattCharacteristic characteristic) {
            byte[] value = characteristic.getValue();
            if (DBG) {
                Log.d(TAG, "value " + value);
            }
            if (value != null) {
                Log.e(TAG, "value null");
                value = Arrays.copyOfRange(value, offset, value.length);
            }
            mBluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS,
                    offset, value);
        }
    };

     /**
     * A proxy class that facilitates testing.
     *
     * This is necessary due to the "final" attribute of the BluetoothGattServer class.
     */
    public static class BluetoothGattServerProxy {
        private final Context mContext;
        private final BluetoothManager mBluetoothManager;

        private BluetoothGattServer mBluetoothGattServer;

         /**
          * Create a new GATT server proxy object
          * @param context context to use
          */
        public BluetoothGattServerProxy(Context context) {
            mContext = context;
            mBluetoothManager = context.getSystemService(BluetoothManager.class);
        }

         /**
          * Open with GATT server callback
          * @param callback callback to invoke
          * @return true on success
          */
        public boolean open(BluetoothGattServerCallback callback) {
            mBluetoothGattServer = mBluetoothManager.openGattServer(mContext, callback);
            return mBluetoothGattServer != null;
        }

         /**
          * Close the GATT server, should be called as soon as the server is not needed
          */
        public void close() {
            if (mBluetoothGattServer == null) {
                Log.w(TAG, "BluetoothGattServerProxy.close() called without open()");
                return;
            }
            mBluetoothGattServer.close();
            mBluetoothGattServer = null;
        }

         /**
          * Add a GATT service
          * @param service added service
          * @return true on success
          */
        public boolean addService(BluetoothGattService service) {
            return mBluetoothGattServer.addService(service);
        }

         /**
          * Send GATT response to remote
          * @param device remote device
          * @param requestId request id
          * @param status status of response
          * @param offset offset of the value
          * @param value value content
          * @return true on success
          */
        public boolean sendResponse(
                BluetoothDevice device, int requestId, int status, int offset, byte[] value) {
            return mBluetoothGattServer.sendResponse(device, requestId, status, offset, value);
        }

         /**
          * Gatt a list of devices connected to this GATT server
          * @return list of connected devices at this moment
          */
        public List<BluetoothDevice> getConnectedDevices() {
            return mBluetoothManager.getConnectedDevices(BluetoothProfile.GATT_SERVER);
        }
    }
}
+9 −0
Original line number Diff line number Diff line
@@ -45,6 +45,7 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.Spy;

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeoutException;
@@ -70,6 +71,8 @@ public class LeAudioBroadcastServiceTest {
    private AudioManager mAudioManager;
    @Mock
    private LeAudioBroadcasterNativeInterface mNativeInterface;
    @Mock private LeAudioTmapGattServer mTmapGattServer;
    @Spy private LeAudioObjectsFactory mObjectsFactory = LeAudioObjectsFactory.getInstance();

    private static final String TEST_MAC_ADDRESS = "00:11:22:33:44:55";
    private static final int TEST_BROADCAST_ID = 42;
@@ -155,6 +158,12 @@ public class LeAudioBroadcastServiceTest {
        // Set up mocks and test assets
        MockitoAnnotations.initMocks(this);

        // Use spied objects factory
        doNothing().when(mTmapGattServer).start(anyInt());
        doNothing().when(mTmapGattServer).stop();
        LeAudioObjectsFactory.setInstanceForTesting(mObjectsFactory);
        doReturn(mTmapGattServer).when(mObjectsFactory).getTmapGattServer(any());

        if (Looper.myLooper() == null) {
            Looper.prepare();
        }
+15 −10
Original line number Diff line number Diff line
@@ -24,18 +24,16 @@ import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.anyString;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.nullable;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.eq;

import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothLeAudio;
import android.bluetooth.BluetoothLeAudioCodecConfig;
import android.bluetooth.BluetoothLeAudioCodecStatus;
import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothUuid;
import android.bluetooth.IBluetoothLeAudioCallback;
@@ -44,7 +42,6 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.media.AudioManager;
import android.media.BluetoothProfileConnectionInfo;
import android.os.ParcelUuid;

import androidx.test.InstrumentationRegistry;
@@ -63,13 +60,11 @@ import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.Spy;

import java.util.Arrays;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@@ -104,6 +99,9 @@ public class LeAudioServiceTest {
    @Mock private DatabaseManager mDatabaseManager;
    @Mock private LeAudioNativeInterface mNativeInterface;
    @Mock private AudioManager mAudioManager;
    @Mock private LeAudioTmapGattServer mTmapGattServer;
    @Spy private LeAudioObjectsFactory mObjectsFactory = LeAudioObjectsFactory.getInstance();


    @Rule public final ServiceTestRule mServiceRule = new ServiceTestRule();

@@ -135,7 +133,6 @@ public class LeAudioServiceTest {
                    add(LC3_48KHZ_16KHZ_CONFIG);
            }};


    private static final List<BluetoothLeAudioCodecConfig> INPUT_SELECTABLE_CONFIG =
            new ArrayList() {{
                    add(LC3_16KHZ_CONFIG);
@@ -153,6 +150,12 @@ public class LeAudioServiceTest {
        // Set up mocks and test assets
        MockitoAnnotations.initMocks(this);

        // Use spied objects factory
        doNothing().when(mTmapGattServer).start(anyInt());
        doNothing().when(mTmapGattServer).stop();
        LeAudioObjectsFactory.setInstanceForTesting(mObjectsFactory);
        doReturn(mTmapGattServer).when(mObjectsFactory).getTmapGattServer(any());

        TestUtils.setAdapterService(mAdapterService);
        doReturn(MAX_LE_AUDIO_CONNECTIONS).when(mAdapterService).getMaxConnectedAudioDevices();
        doReturn(new ParcelUuid[]{BluetoothUuid.LE_AUDIO}).when(mAdapterService)
@@ -160,7 +163,9 @@ public class LeAudioServiceTest {
        doReturn(mDatabaseManager).when(mAdapterService).getDatabase();
        doReturn(true, false).when(mAdapterService).isStartedProfile(anyString());

        mAdapter = BluetoothAdapter.getDefaultAdapter();
        BluetoothManager manager = mTargetContext.getSystemService(BluetoothManager.class);
        assertThat(manager).isNotNull();
        mAdapter = manager.getAdapter();
        // Mock methods in AdapterService
        doAnswer(invocation -> mBondedDevices.toArray(new BluetoothDevice[]{})).when(
                mAdapterService).getBondedDevices();
Loading