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

Commit 7f47feff authored by Jakub Tyszkowski's avatar Jakub Tyszkowski
Browse files

LeAudio: Allow GMCS to control the Live context

With this change, the generic MCS declares control
over the "Live" context. This can be used by the recorder app
to implement the media session controller.

This adds support to Content Control Id Keeper to hold
multiple contexts under the same CCID value, and by this allows
a single control service to declare control over multiple audio
contexts. This information is then used in stream establishment so
that the remote would know which control service to use for stream
control.

Additionally it
 - removes 'virtual' declaration as Pimpl design pattern is used for
   compile time function call dispatch
 - disambiguates native API by using the proper type for a single
   and complex context type
 - properly support multiple contexts at the top and native layer
   resulting in proper entries for each individual context
 - improves robustness by validating context input parameter
 - improves robustness for multple calls with the same client UUID
   (previously we were leaking ccid values from the ccid pool)
 - moves the helper function used for getting CCID list for the complex
   context types into the CCID keeper
 - adds java code unit tests
 - adds native code unit tests

Bug: 262488856
Tag: #feature
Test: atest ContentControlIdKeeperTest bluetooth_le_audio_test bluetooth_test_broadcaster --no-bazel-mode
Change-Id: Iadcd75023c71b8ee76950633c7c779dc5ce2e6d1
parent 064b49b4
Loading
Loading
Loading
Loading
+53 −7
Original line number Diff line number Diff line
@@ -19,6 +19,7 @@ package com.android.bluetooth.le_audio;

import android.bluetooth.BluetoothLeAudio;
import android.os.ParcelUuid;
import android.util.Log;
import android.util.Pair;

import com.android.bluetooth.btservice.ServiceFactory;
@@ -26,6 +27,8 @@ import com.android.bluetooth.btservice.ServiceFactory;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.SortedSet;
import java.util.TreeSet;

@@ -33,15 +36,23 @@ import java.util.TreeSet;
 * This class keeps Content Control Ids for LE Audio profiles.
 */
public class ContentControlIdKeeper {
    private static final String TAG = "ContentControlIdKeeper";

    public static final int CCID_INVALID = 0;
    public static final int CCID_MIN = 0x01;
    public static final int CCID_MAX = 0xFF;

    private static SortedSet<Integer> sAssignedCcidList = new TreeSet();
    private static HashMap<ParcelUuid, Pair<Integer, Integer>> sUserMap = new HashMap();
    private static HashMap<ParcelUuid, Pair<Integer, Integer>> sUuidToCcidContextPair =
            new HashMap();
    private static ServiceFactory sServiceFactory = null;

    static synchronized void initForTesting(ServiceFactory instance) {
        sAssignedCcidList = new TreeSet();
        sUuidToCcidContextPair = new HashMap();
        sServiceFactory = instance;
    }

    /**
     * Functions is used to acquire Content Control ID (Ccid). Ccid is connected
     * with a context type  and the user uuid. In most of cases user uuid is the GATT service
@@ -53,6 +64,16 @@ public class ContentControlIdKeeper {
     */
    public static synchronized int acquireCcid(ParcelUuid userUuid, int contextType) {
        int ccid = CCID_INVALID;
        if (contextType == BluetoothLeAudio.CONTEXT_TYPE_INVALID) {
            Log.e(TAG, "Invalid context type value: " + contextType);
            return ccid;
        }

        // Remove any previous mapping
        Pair<Integer, Integer> ccidContextPair = sUuidToCcidContextPair.get(userUuid);
        if (ccidContextPair != null) {
            releaseCcid(ccidContextPair.first);
        }

        if (sAssignedCcidList.size() == 0) {
            ccid = CCID_MIN;
@@ -73,7 +94,7 @@ public class ContentControlIdKeeper {

        if (ccid != CCID_INVALID)  {
            sAssignedCcidList.add(ccid);
            sUserMap.put(userUuid, new Pair(ccid, contextType));
            sUuidToCcidContextPair.put(userUuid, new Pair(ccid, contextType));

            if (sServiceFactory == null) {
                sServiceFactory = new ServiceFactory();
@@ -93,8 +114,32 @@ public class ContentControlIdKeeper {
     * @param value Ccid value to release
     */
    public static synchronized void releaseCcid(int value) {
        ParcelUuid uuid = null;

        for (Entry entry : sUuidToCcidContextPair.entrySet()) {
            if (Objects.equals(value, ((Pair<Integer, Integer>) entry.getValue()).first)) {
                uuid = (ParcelUuid) entry.getKey();
                break;
            }
        }
        if (uuid == null) {
            Log.e(TAG, "Tried to remove an unknown CCID: " + value);
            return;
        }

        if (sAssignedCcidList.contains(value)) {
            if (sServiceFactory == null) {
                sServiceFactory = new ServiceFactory();
            }
            /* Notify LeAudioService about new value  */
            LeAudioService service = sServiceFactory.getLeAudioService();
            if (service != null) {
                service.setCcidInformation(uuid, value, 0);
            }

            sAssignedCcidList.remove(value);
        sUserMap.entrySet().removeIf(entry -> entry.getValue().first.equals(value));
            sUuidToCcidContextPair.remove(uuid);
        }
    }

    /**
@@ -102,7 +147,8 @@ public class ContentControlIdKeeper {
     *
     * @return Map of acquired ccids along with the user information.
     */
    public static synchronized Map<ParcelUuid, Pair<Integer, Integer>> getUserCcidMap() {
        return Collections.unmodifiableMap(sUserMap);
    public static synchronized Map<ParcelUuid, Pair<Integer, Integer>>
            getUuidToCcidContextPairMap() {
        return Collections.unmodifiableMap(sUuidToCcidContextPair);
    }
}
+1 −1
Original line number Diff line number Diff line
@@ -1551,7 +1551,7 @@ public class LeAudioService extends ProfileService {
        } else if (stackEvent.type == LeAudioStackEvent.EVENT_TYPE_NATIVE_INITIALIZED) {
            mLeAudioNativeIsInitialized = true;
            for (Map.Entry<ParcelUuid, Pair<Integer, Integer>> entry :
                    ContentControlIdKeeper.getUserCcidMap().entrySet()) {
                    ContentControlIdKeeper.getUuidToCcidContextPairMap().entrySet()) {
                ParcelUuid userUuid = entry.getKey();
                Pair<Integer, Integer> ccidInformation = entry.getValue();
                setCcidInformation(userUuid, ccidInformation.first, ccidInformation.second);
+1 −1
Original line number Diff line number Diff line
@@ -677,7 +677,7 @@ public class MediaControlProfile implements MediaControlServiceCallbacks {

            // Instantiate a Service Instance and it's state machine
            int ccid = ContentControlIdKeeper.acquireCcid(BluetoothUuid.GENERIC_MEDIA_CONTROL,
                    BluetoothLeAudio.CONTEXT_TYPE_MEDIA);
                    BluetoothLeAudio.CONTEXT_TYPE_MEDIA | BluetoothLeAudio.CONTEXT_TYPE_LIVE);
            if (ccid == ContentControlIdKeeper.CCID_INVALID) {
                Log.e(TAG, "Unable to acquire valid CCID!");
                return;
+144 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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 static org.mockito.Mockito.*;

import android.bluetooth.BluetoothLeAudio;
import android.os.ParcelUuid;

import android.os.ParcelUuid;
import android.util.Pair;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.MediumTest;
import androidx.test.runner.AndroidJUnit4;

import com.android.bluetooth.btservice.ServiceFactory;

import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import java.util.Map;
import java.util.UUID;

@MediumTest
@RunWith(AndroidJUnit4.class)
public class ContentControlIdKeeperTest {
    @Mock
    ServiceFactory mServiceFactoryMock;
    @Mock
    LeAudioService mLeAudioServiceMock;

    @Before
    public void setUp() throws Exception {
        MockitoAnnotations.initMocks(this);

        doReturn(mLeAudioServiceMock).when(mServiceFactoryMock).getLeAudioService();
        ContentControlIdKeeper.initForTesting(mServiceFactoryMock);
    }

    @After
    public void tearDown() throws Exception {
        ContentControlIdKeeper.initForTesting(null);
    }

    public int testCcidAcquire(ParcelUuid uuid, int context, int expectedListSize) {
        int ccid = ContentControlIdKeeper.acquireCcid(uuid, context);
        Assert.assertNotEquals(
                ccid,
                ContentControlIdKeeper.CCID_INVALID);

        verify(mLeAudioServiceMock).setCcidInformation(eq(uuid),
                        eq(ccid), eq(context));
        Map<ParcelUuid, Pair<Integer, Integer>> uuidToCcidContextPair =
                ContentControlIdKeeper.getUuidToCcidContextPairMap();
        Assert.assertEquals(expectedListSize, uuidToCcidContextPair.size());
        Assert.assertTrue(uuidToCcidContextPair.containsKey(uuid));
        Assert.assertEquals(ccid, (long)uuidToCcidContextPair.get(uuid).first);
        Assert.assertEquals(context, (long)uuidToCcidContextPair.get(uuid).second);

        return ccid;
    }

    public void testCcidRelease(ParcelUuid uuid, int ccid, int expectedListSize) {
        Map<ParcelUuid, Pair<Integer, Integer>> uuidToCcidContextPair =
                        ContentControlIdKeeper.getUuidToCcidContextPairMap();
        Assert.assertTrue(uuidToCcidContextPair.containsKey(uuid));

        ContentControlIdKeeper.releaseCcid(ccid);
        uuidToCcidContextPair = ContentControlIdKeeper.getUuidToCcidContextPairMap();
        Assert.assertFalse(uuidToCcidContextPair.containsKey(uuid));

        verify(mLeAudioServiceMock).setCcidInformation(eq(uuid),
                eq(ccid), eq(0));

        Assert.assertEquals(expectedListSize, uuidToCcidContextPair.size());
    }

    @Test
    public void testAcquireReleaseCcid() {
        ParcelUuid uuid_one = new ParcelUuid(UUID.randomUUID());
        ParcelUuid uuid_two = new ParcelUuid(UUID.randomUUID());

        int ccid_one = testCcidAcquire(uuid_one, BluetoothLeAudio.CONTEXT_TYPE_MEDIA, 1);
        int ccid_two = testCcidAcquire(uuid_two, BluetoothLeAudio.CONTEXT_TYPE_RINGTONE, 2);
        Assert.assertNotEquals(ccid_one, ccid_two);

        testCcidRelease(uuid_one, ccid_one, 1);
        testCcidRelease(uuid_two, ccid_two, 0);
    }

    @Test
    public void testAcquireReleaseCcidForCompoundContext() {
        ParcelUuid uuid = new ParcelUuid(UUID.randomUUID());
        int ccid = testCcidAcquire(uuid,
                BluetoothLeAudio.CONTEXT_TYPE_MEDIA | BluetoothLeAudio.CONTEXT_TYPE_RINGTONE, 1);
        testCcidRelease(uuid, ccid, 0);
    }

    @Test
    public void testAcquireInvalidContext() {
        ParcelUuid uuid = new ParcelUuid(UUID.randomUUID());

        int ccid = ContentControlIdKeeper.acquireCcid(uuid, 0);
        Assert.assertEquals(ccid, ContentControlIdKeeper.CCID_INVALID);

        verify(mLeAudioServiceMock,
                times(0)).setCcidInformation(any(ParcelUuid.class), any(int.class), any(int.class));
        Map<ParcelUuid, Pair<Integer, Integer>> uuidToCcidContextPair =
                ContentControlIdKeeper.getUuidToCcidContextPairMap();
        Assert.assertEquals(0, uuidToCcidContextPair.size());
    }

    @Test
    public void testAcquireContextMoreThanOnce() {
        ParcelUuid uuid = new ParcelUuid(UUID.randomUUID());

        int ccid_one = testCcidAcquire(uuid, BluetoothLeAudio.CONTEXT_TYPE_MEDIA, 1);
        int ccid_two = testCcidAcquire(uuid, BluetoothLeAudio.CONTEXT_TYPE_RINGTONE, 1);

        // This is implementation specific but verifies that the previous CCID was recycled
        Assert.assertEquals(ccid_one, ccid_two);
    }

}
+6 −0
Original line number Diff line number Diff line
@@ -223,6 +223,12 @@ public final class BluetoothLeAudio implements BluetoothProfile, AutoCloseable {
    public static final String ACTION_LE_AUDIO_ACTIVE_DEVICE_CHANGED =
            "android.bluetooth.action.LE_AUDIO_ACTIVE_DEVICE_CHANGED";

    /**
     * Indicates invalid/unset audio context.
     * @hide
     */
    public static final int CONTEXT_TYPE_INVALID = 0x0000;

    /**
     * Indicates unspecified audio content.
     * @hide
Loading