| /* |
| * Copyright 2021 HIMSA II K/S - www.himsa.com. |
| * Represented by EHIMA - www.ehima.com |
| * |
| * 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.annotation.Nullable; |
| import android.bluetooth.*; |
| import android.content.BroadcastReceiver; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.media.AudioManager; |
| import android.os.Looper; |
| |
| import androidx.test.InstrumentationRegistry; |
| import androidx.test.filters.MediumTest; |
| import androidx.test.rule.ServiceTestRule; |
| import androidx.test.runner.AndroidJUnit4; |
| |
| import com.android.bluetooth.TestUtils; |
| import com.android.bluetooth.btservice.AdapterService; |
| import com.android.bluetooth.btservice.storage.DatabaseManager; |
| |
| import org.junit.After; |
| import org.junit.Assert; |
| import org.junit.Before; |
| import org.junit.Rule; |
| 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; |
| import java.util.List; |
| |
| @MediumTest |
| @RunWith(AndroidJUnit4.class) |
| public class LeAudioBroadcastServiceTest { |
| private static final int TIMEOUT_MS = 1000; |
| @Rule |
| public final ServiceTestRule mServiceRule = new ServiceTestRule(); |
| private BluetoothAdapter mAdapter; |
| private BluetoothDevice mDevice; |
| private Context mTargetContext; |
| private LeAudioService mService; |
| private LeAudioIntentReceiver mLeAudioIntentReceiver; |
| private LinkedBlockingQueue<Intent> mIntentQueue; |
| @Mock |
| private AdapterService mAdapterService; |
| @Mock |
| private DatabaseManager mDatabaseManager; |
| @Mock |
| private AudioManager mAudioManager; |
| @Mock private LeAudioBroadcasterNativeInterface mLeAudioBroadcasterNativeInterface; |
| @Mock private LeAudioNativeInterface mLeAudioNativeInterface; |
| @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; |
| private static final int TEST_ADVERTISER_SID = 1234; |
| private static final int TEST_PA_SYNC_INTERVAL = 100; |
| private static final int TEST_PRESENTATION_DELAY_MS = 345; |
| |
| private static final int TEST_CODEC_ID = 42; |
| private static final int TEST_CHANNEL_INDEX = 56; |
| |
| // For BluetoothLeAudioCodecConfigMetadata |
| private static final long TEST_AUDIO_LOCATION_FRONT_LEFT = 0x01; |
| private static final long TEST_AUDIO_LOCATION_FRONT_RIGHT = 0x02; |
| |
| // For BluetoothLeAudioContentMetadata |
| private static final String TEST_PROGRAM_INFO = "Test"; |
| // German language code in ISO 639-3 |
| private static final String TEST_LANGUAGE = "deu"; |
| private static final String TEST_BROADCAST_NAME = "Name Test"; |
| |
| private boolean mOnBroadcastStartedCalled = false; |
| private boolean mOnBroadcastStartFailedCalled = false; |
| private boolean mOnBroadcastStoppedCalled = false; |
| private boolean mOnBroadcastStopFailedCalled = false; |
| private boolean mOnPlaybackStartedCalled = false; |
| private boolean mOnPlaybackStoppedCalled = false; |
| private boolean mOnBroadcastUpdatedCalled = false; |
| private boolean mOnBroadcastUpdateFailedCalled = false; |
| private boolean mOnBroadcastMetadataChangedCalled = false; |
| |
| private final IBluetoothLeBroadcastCallback mCallbacks = |
| new IBluetoothLeBroadcastCallback.Stub() { |
| @Override |
| public void onBroadcastStarted(int reason, int broadcastId) { |
| mOnBroadcastStartedCalled = true; |
| } |
| |
| @Override |
| public void onBroadcastStartFailed(int reason) { |
| mOnBroadcastStartFailedCalled = true; |
| } |
| |
| @Override |
| public void onBroadcastStopped(int reason, int broadcastId) { |
| mOnBroadcastStoppedCalled = true; |
| } |
| |
| @Override |
| public void onBroadcastStopFailed(int reason) { |
| mOnBroadcastStopFailedCalled = true; |
| } |
| |
| @Override |
| public void onPlaybackStarted(int reason, int broadcastId) { |
| mOnPlaybackStartedCalled = true; |
| } |
| |
| @Override |
| public void onPlaybackStopped(int reason, int broadcastId) { |
| mOnPlaybackStoppedCalled = true; |
| } |
| |
| @Override |
| public void onBroadcastUpdated(int reason, int broadcastId) { |
| mOnBroadcastUpdatedCalled = true; |
| } |
| |
| @Override |
| public void onBroadcastUpdateFailed(int reason, int broadcastId) { |
| mOnBroadcastUpdateFailedCalled = true; |
| } |
| |
| @Override |
| public void onBroadcastMetadataChanged(int broadcastId, |
| BluetoothLeBroadcastMetadata metadata) { |
| mOnBroadcastMetadataChangedCalled = true; |
| } |
| }; |
| |
| @Before |
| public void setUp() throws Exception { |
| mTargetContext = InstrumentationRegistry.getTargetContext(); |
| |
| // 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(); |
| } |
| |
| TestUtils.setAdapterService(mAdapterService); |
| doReturn(mDatabaseManager).when(mAdapterService).getDatabase(); |
| doReturn(true, false).when(mAdapterService).isStartedProfile(anyString()); |
| doReturn(true).when(mAdapterService).isLeAudioBroadcastSourceSupported(); |
| doReturn((long)(1 << BluetoothProfile.LE_AUDIO_BROADCAST) | (1 << BluetoothProfile.LE_AUDIO)) |
| .when(mAdapterService).getSupportedProfilesBitMask(); |
| |
| mAdapter = BluetoothAdapter.getDefaultAdapter(); |
| |
| LeAudioBroadcasterNativeInterface.setInstance(mLeAudioBroadcasterNativeInterface); |
| LeAudioNativeInterface.setInstance(mLeAudioNativeInterface); |
| startService(); |
| mService.mAudioManager = mAudioManager; |
| |
| // Set up the State Changed receiver |
| IntentFilter filter = new IntentFilter(); |
| filter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY); |
| |
| mLeAudioIntentReceiver = new LeAudioIntentReceiver(); |
| mTargetContext.registerReceiver(mLeAudioIntentReceiver, filter); |
| |
| mDevice = TestUtils.getTestDevice(mAdapter, 0); |
| when(mLeAudioBroadcasterNativeInterface.getDevice(any(byte[].class))).thenReturn(mDevice); |
| |
| mIntentQueue = new LinkedBlockingQueue<Intent>(); |
| } |
| |
| @After |
| public void tearDown() throws Exception { |
| if (mService == null) { |
| return; |
| } |
| |
| stopService(); |
| LeAudioBroadcasterNativeInterface.setInstance(null); |
| LeAudioNativeInterface.setInstance(null); |
| mTargetContext.unregisterReceiver(mLeAudioIntentReceiver); |
| TestUtils.clearAdapterService(mAdapterService); |
| reset(mAudioManager); |
| } |
| |
| private void startService() throws TimeoutException { |
| TestUtils.startService(mServiceRule, LeAudioService.class); |
| mService = LeAudioService.getLeAudioService(); |
| Assert.assertNotNull(mService); |
| } |
| |
| private void stopService() throws TimeoutException { |
| TestUtils.stopService(mServiceRule, LeAudioService.class); |
| mService = LeAudioService.getLeAudioService(); |
| Assert.assertNull(mService); |
| } |
| |
| /** |
| * Test getting LeAudio Service |
| */ |
| @Test |
| public void testGetLeAudioService() { |
| Assert.assertEquals(mService, LeAudioService.getLeAudioService()); |
| } |
| |
| @Test |
| public void testStopLeAudioService() { |
| Assert.assertEquals(mService, LeAudioService.getLeAudioService()); |
| |
| InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() { |
| public void run() { |
| Assert.assertTrue(mService.stop()); |
| } |
| }); |
| InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() { |
| public void run() { |
| Assert.assertTrue(mService.start()); |
| } |
| }); |
| } |
| |
| void verifyBroadcastStarted(int broadcastId, BluetoothLeBroadcastSettings settings) { |
| mService.createBroadcast(settings); |
| |
| List<BluetoothLeBroadcastSubgroupSettings> settingsList = |
| settings.getSubgroupSettings(); |
| |
| int[] expectedQualityArray = |
| settingsList.stream() |
| .mapToInt(setting -> setting.getPreferredQuality()).toArray(); |
| byte[][] expectedDataArray = |
| settingsList.stream() |
| .map(setting -> setting.getContentMetadata().getRawMetadata()) |
| .toArray(byte[][]::new); |
| |
| verify(mLeAudioBroadcasterNativeInterface, times(1)) |
| .createBroadcast( |
| eq(true), |
| eq(TEST_BROADCAST_NAME), |
| eq(settings.getBroadcastCode()), |
| eq(settings.getPublicBroadcastMetadata().getRawMetadata()), |
| eq(expectedQualityArray), |
| eq(expectedDataArray)); |
| |
| // Check if broadcast is started automatically when created |
| LeAudioStackEvent create_event = |
| new LeAudioStackEvent(LeAudioStackEvent.EVENT_TYPE_BROADCAST_CREATED); |
| create_event.valueInt1 = broadcastId; |
| create_event.valueBool1 = true; |
| mService.messageFromNative(create_event); |
| |
| // Verify if broadcast is auto-started on start |
| verify(mLeAudioBroadcasterNativeInterface, times(1)).startBroadcast(eq(broadcastId)); |
| |
| // Notify initial paused state |
| LeAudioStackEvent state_event = |
| new LeAudioStackEvent(LeAudioStackEvent.EVENT_TYPE_BROADCAST_STATE); |
| state_event.valueInt1 = broadcastId; |
| state_event.valueInt2 = LeAudioStackEvent.BROADCAST_STATE_PAUSED; |
| mService.messageFromNative(state_event); |
| |
| // Switch to active streaming |
| state_event = new LeAudioStackEvent(LeAudioStackEvent.EVENT_TYPE_BROADCAST_STATE); |
| state_event.valueInt1 = broadcastId; |
| state_event.valueInt2 = LeAudioStackEvent.BROADCAST_STATE_STREAMING; |
| mService.messageFromNative(state_event); |
| |
| // Check if metadata is requested when the broadcast starts to stream |
| verify(mLeAudioBroadcasterNativeInterface, times(1)).getBroadcastMetadata(eq(broadcastId)); |
| Assert.assertFalse(mOnBroadcastStartFailedCalled); |
| Assert.assertTrue(mOnBroadcastStartedCalled); |
| } |
| |
| void verifyBroadcastStopped(int broadcastId) { |
| mService.stopBroadcast(broadcastId); |
| verify(mLeAudioBroadcasterNativeInterface, times(1)).stopBroadcast(eq(broadcastId)); |
| |
| LeAudioStackEvent state_event = |
| new LeAudioStackEvent(LeAudioStackEvent.EVENT_TYPE_BROADCAST_STATE); |
| state_event.valueInt1 = broadcastId; |
| state_event.valueInt2 = LeAudioStackEvent.BROADCAST_STATE_STOPPED; |
| mService.messageFromNative(state_event); |
| |
| // Verify if broadcast is auto-destroyed on stop |
| verify(mLeAudioBroadcasterNativeInterface, times(1)).destroyBroadcast(eq(broadcastId)); |
| |
| state_event = new LeAudioStackEvent(LeAudioStackEvent.EVENT_TYPE_BROADCAST_DESTROYED); |
| state_event.valueInt1 = broadcastId; |
| mService.messageFromNative(state_event); |
| |
| Assert.assertTrue(mOnBroadcastStoppedCalled); |
| Assert.assertFalse(mOnBroadcastStopFailedCalled); |
| } |
| |
| @Test |
| public void testCreateBroadcastNative() { |
| int broadcastId = 243; |
| byte[] code = {0x00, 0x01, 0x00, 0x02}; |
| |
| mService.mBroadcastCallbacks.register(mCallbacks); |
| |
| BluetoothLeAudioContentMetadata.Builder meta_builder = |
| new BluetoothLeAudioContentMetadata.Builder(); |
| meta_builder.setLanguage("deu"); |
| meta_builder.setProgramInfo("Subgroup broadcast info"); |
| BluetoothLeAudioContentMetadata meta = meta_builder.build(); |
| |
| verifyBroadcastStarted(broadcastId, buildBroadcastSettingsFromMetadata(meta, code, 1)); |
| } |
| |
| @Test |
| public void testCreateBroadcastNativeMultiGroups() { |
| int broadcastId = 243; |
| byte[] code = {0x00, 0x01, 0x00, 0x02}; |
| |
| mService.mBroadcastCallbacks.register(mCallbacks); |
| |
| BluetoothLeAudioContentMetadata.Builder meta_builder = |
| new BluetoothLeAudioContentMetadata.Builder(); |
| meta_builder.setLanguage("deu"); |
| meta_builder.setProgramInfo("Subgroup broadcast info"); |
| BluetoothLeAudioContentMetadata meta = meta_builder.build(); |
| |
| verifyBroadcastStarted(broadcastId, buildBroadcastSettingsFromMetadata(meta, code, 3)); |
| } |
| |
| @Test |
| public void testCreateBroadcastNativeFailed() { |
| int broadcastId = 243; |
| byte[] code = {0x00, 0x01, 0x00, 0x02}; |
| |
| mService.mBroadcastCallbacks.register(mCallbacks); |
| |
| BluetoothLeAudioContentMetadata.Builder meta_builder = |
| new BluetoothLeAudioContentMetadata.Builder(); |
| meta_builder.setLanguage("deu"); |
| meta_builder.setProgramInfo("Public broadcast info"); |
| BluetoothLeAudioContentMetadata meta = meta_builder.build(); |
| BluetoothLeBroadcastSettings settings = buildBroadcastSettingsFromMetadata(meta, code, 1); |
| mService.createBroadcast(settings); |
| |
| // Test data with only one subgroup |
| int[] expectedQualityArray = |
| {settings.getSubgroupSettings().get(0).getPreferredQuality()}; |
| byte[][] expectedDataArray = |
| {settings.getSubgroupSettings().get(0).getContentMetadata().getRawMetadata()}; |
| |
| verify(mLeAudioBroadcasterNativeInterface, times(1)) |
| .createBroadcast( |
| eq(true), |
| eq(TEST_BROADCAST_NAME), |
| eq(code), |
| eq(settings.getPublicBroadcastMetadata().getRawMetadata()), |
| eq(expectedQualityArray), |
| eq(expectedDataArray)); |
| |
| LeAudioStackEvent create_event = |
| new LeAudioStackEvent(LeAudioStackEvent.EVENT_TYPE_BROADCAST_CREATED); |
| create_event.valueInt1 = broadcastId; |
| create_event.valueBool1 = false; |
| mService.messageFromNative(create_event); |
| |
| Assert.assertFalse(mOnBroadcastStartedCalled); |
| Assert.assertTrue(mOnBroadcastStartFailedCalled); |
| } |
| |
| @Test |
| public void testStartStopBroadcastNative() { |
| int broadcastId = 243; |
| byte[] code = {0x00, 0x01, 0x00, 0x02}; |
| |
| mService.mBroadcastCallbacks.register(mCallbacks); |
| |
| BluetoothLeAudioContentMetadata.Builder meta_builder = |
| new BluetoothLeAudioContentMetadata.Builder(); |
| meta_builder.setLanguage("deu"); |
| meta_builder.setProgramInfo("Subgroup broadcast info"); |
| BluetoothLeAudioContentMetadata meta = meta_builder.build(); |
| |
| verifyBroadcastStarted(broadcastId, buildBroadcastSettingsFromMetadata(meta, code, 1)); |
| verifyBroadcastStopped(broadcastId); |
| } |
| |
| @Test |
| public void testBroadcastInvalidBroadcastIdRequest() { |
| int broadcastId = 243; |
| |
| mService.mBroadcastCallbacks.register(mCallbacks); |
| |
| // Stop non-existing broadcast |
| mService.stopBroadcast(broadcastId); |
| Assert.assertFalse(mOnBroadcastStoppedCalled); |
| Assert.assertTrue(mOnBroadcastStopFailedCalled); |
| |
| // Update metadata for non-existing broadcast |
| BluetoothLeAudioContentMetadata.Builder meta_builder = |
| new BluetoothLeAudioContentMetadata.Builder(); |
| meta_builder.setLanguage("eng"); |
| meta_builder.setProgramInfo("Public broadcast info"); |
| mService.updateBroadcast(broadcastId, |
| buildBroadcastSettingsFromMetadata(meta_builder.build(), null, 1)); |
| Assert.assertFalse(mOnBroadcastUpdatedCalled); |
| Assert.assertTrue(mOnBroadcastUpdateFailedCalled); |
| } |
| |
| private BluetoothLeBroadcastSubgroup createBroadcastSubgroup() { |
| BluetoothLeAudioCodecConfigMetadata codecMetadata = |
| new BluetoothLeAudioCodecConfigMetadata.Builder() |
| .setAudioLocation(TEST_AUDIO_LOCATION_FRONT_LEFT).build(); |
| BluetoothLeAudioContentMetadata contentMetadata = |
| new BluetoothLeAudioContentMetadata.Builder() |
| .setProgramInfo(TEST_PROGRAM_INFO).setLanguage(TEST_LANGUAGE).build(); |
| BluetoothLeBroadcastSubgroup.Builder builder = new BluetoothLeBroadcastSubgroup.Builder() |
| .setCodecId(TEST_CODEC_ID) |
| .setCodecSpecificConfig(codecMetadata) |
| .setContentMetadata(contentMetadata); |
| |
| BluetoothLeAudioCodecConfigMetadata channelCodecMetadata = |
| new BluetoothLeAudioCodecConfigMetadata.Builder() |
| .setAudioLocation(TEST_AUDIO_LOCATION_FRONT_RIGHT).build(); |
| |
| // builder expect at least one channel |
| BluetoothLeBroadcastChannel channel = |
| new BluetoothLeBroadcastChannel.Builder() |
| .setSelected(true) |
| .setChannelIndex(TEST_CHANNEL_INDEX) |
| .setCodecMetadata(channelCodecMetadata) |
| .build(); |
| builder.addChannel(channel); |
| return builder.build(); |
| } |
| |
| private BluetoothLeBroadcastMetadata createBroadcastMetadata() { |
| BluetoothDevice testDevice = |
| mAdapter.getRemoteLeDevice(TEST_MAC_ADDRESS, BluetoothDevice.ADDRESS_TYPE_RANDOM); |
| |
| BluetoothLeBroadcastMetadata.Builder builder = new BluetoothLeBroadcastMetadata.Builder() |
| .setEncrypted(false) |
| .setSourceDevice(testDevice, BluetoothDevice.ADDRESS_TYPE_RANDOM) |
| .setSourceAdvertisingSid(TEST_ADVERTISER_SID) |
| .setBroadcastId(TEST_BROADCAST_ID) |
| .setBroadcastCode(null) |
| .setPaSyncInterval(TEST_PA_SYNC_INTERVAL) |
| .setPresentationDelayMicros(TEST_PRESENTATION_DELAY_MS); |
| // builder expect at least one subgroup |
| builder.addSubgroup(createBroadcastSubgroup()); |
| return builder.build(); |
| } |
| |
| @Test |
| public void testGetAllBroadcastMetadata() { |
| int broadcastId = 243; |
| byte[] code = {0x00, 0x01, 0x00, 0x02}; |
| |
| BluetoothLeAudioContentMetadata.Builder meta_builder = |
| new BluetoothLeAudioContentMetadata.Builder(); |
| meta_builder.setLanguage("ENG"); |
| meta_builder.setProgramInfo("Public broadcast info"); |
| BluetoothLeAudioContentMetadata meta = meta_builder.build(); |
| mService.createBroadcast(buildBroadcastSettingsFromMetadata(meta, code, 1)); |
| |
| LeAudioStackEvent create_event = |
| new LeAudioStackEvent(LeAudioStackEvent.EVENT_TYPE_BROADCAST_CREATED); |
| create_event.valueInt1 = broadcastId; |
| create_event.valueBool1 = true; |
| mService.messageFromNative(create_event); |
| |
| // Inject metadata stack event and verify if getter API works as expected |
| LeAudioStackEvent state_event = |
| new LeAudioStackEvent(LeAudioStackEvent.EVENT_TYPE_BROADCAST_METADATA_CHANGED); |
| state_event.valueInt1 = broadcastId; |
| state_event.broadcastMetadata = createBroadcastMetadata(); |
| mService.messageFromNative(state_event); |
| |
| List<BluetoothLeBroadcastMetadata> meta_list = mService.getAllBroadcastMetadata(); |
| Assert.assertNotNull(meta_list); |
| Assert.assertNotEquals(meta_list.size(), 0); |
| Assert.assertEquals(meta_list.get(0), state_event.broadcastMetadata); |
| } |
| |
| private class LeAudioIntentReceiver extends BroadcastReceiver { |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| try { |
| mIntentQueue.put(intent); |
| } catch (InterruptedException e) { |
| Assert.fail("Cannot add Intent to the queue: " + e.getMessage()); |
| } |
| } |
| } |
| |
| private BluetoothLeBroadcastSettings buildBroadcastSettingsFromMetadata( |
| BluetoothLeAudioContentMetadata contentMetadata, |
| @Nullable byte[] broadcastCode, |
| int numOfGroups) { |
| BluetoothLeAudioContentMetadata.Builder publicMetaBuilder = |
| new BluetoothLeAudioContentMetadata.Builder(); |
| publicMetaBuilder.setProgramInfo("Public broadcast info"); |
| |
| BluetoothLeBroadcastSubgroupSettings.Builder subgroupBuilder = |
| new BluetoothLeBroadcastSubgroupSettings.Builder() |
| .setContentMetadata(contentMetadata); |
| |
| BluetoothLeBroadcastSettings.Builder builder = new BluetoothLeBroadcastSettings.Builder() |
| .setPublicBroadcast(true) |
| .setBroadcastName(TEST_BROADCAST_NAME) |
| .setBroadcastCode(broadcastCode) |
| .setPublicBroadcastMetadata(publicMetaBuilder.build()); |
| // builder expect at least one subgroup setting |
| for (int i = 0; i < numOfGroups; i++) { |
| // add subgroup settings with the same content |
| builder.addSubgroupSettings(subgroupBuilder.build()); |
| } |
| return builder.build(); |
| } |
| } |