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

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

Merge "LeAudio: Allow GMCS to control the Live context"

parents ac935ba7 7f47feff
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