Loading android/app/src/com/android/bluetooth/le_audio/ContentControlIdKeeper.java +53 −7 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -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 Loading @@ -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; Loading @@ -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(); Loading @@ -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); } } /** Loading @@ -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); } } android/app/src/com/android/bluetooth/le_audio/LeAudioService.java +1 −1 Original line number Diff line number Diff line Loading @@ -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); Loading android/app/src/com/android/bluetooth/mcp/MediaControlProfile.java +1 −1 Original line number Diff line number Diff line Loading @@ -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; Loading android/app/tests/unit/src/com/android/bluetooth/le_audio/ContentControlIdKeeperTest.java 0 → 100644 +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); } } framework/java/android/bluetooth/BluetoothLeAudio.java +6 −0 Original line number Diff line number Diff line Loading @@ -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 Loading
android/app/src/com/android/bluetooth/le_audio/ContentControlIdKeeper.java +53 −7 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -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 Loading @@ -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; Loading @@ -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(); Loading @@ -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); } } /** Loading @@ -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); } }
android/app/src/com/android/bluetooth/le_audio/LeAudioService.java +1 −1 Original line number Diff line number Diff line Loading @@ -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); Loading
android/app/src/com/android/bluetooth/mcp/MediaControlProfile.java +1 −1 Original line number Diff line number Diff line Loading @@ -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; Loading
android/app/tests/unit/src/com/android/bluetooth/le_audio/ContentControlIdKeeperTest.java 0 → 100644 +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); } }
framework/java/android/bluetooth/BluetoothLeAudio.java +6 −0 Original line number Diff line number Diff line Loading @@ -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