diff --git a/android/app/src/com/android/bluetooth/le_audio/LeAudioService.java b/android/app/src/com/android/bluetooth/le_audio/LeAudioService.java index 8ccf2eef231fca143fb4240222add59e14205da8..fa16b4ec7c4925ea455f209e2051e69c3144db7f 100644 --- a/android/app/src/com/android/bluetooth/le_audio/LeAudioService.java +++ b/android/app/src/com/android/bluetooth/le_audio/LeAudioService.java @@ -147,6 +147,9 @@ public class LeAudioService extends ProfileService { .setSampleRate(BluetoothLeAudioCodecConfig.SAMPLE_RATE_48000) .build(); + /* 5 seconds timeout for Broadcast streaming state transition */ + private static final int DIALING_OUT_TIMEOUT_MS = 5000; + private AdapterService mAdapterService; private DatabaseManager mDatabaseManager; private HandlerThread mStateMachinesThread; @@ -167,6 +170,7 @@ public class LeAudioService extends ProfileService { boolean mBluetoothEnabled = false; BluetoothDevice mHfpHandoverDevice = null; LeAudioBroadcasterNativeInterface mLeAudioBroadcasterNativeInterface = null; + private DialingOutTimeoutEvent mDialingOutTimeoutEvent = null; @VisibleForTesting AudioManager mAudioManager; LeAudioTmapGattServer mTmapGattServer; @@ -428,6 +432,8 @@ public class LeAudioService extends ProfileService { mAwaitingBroadcastCreateResponse = false; mIsSourceStreamMonitorModeEnabled = false; + clearBroadcastTimeoutCallback(); + mHandler.removeCallbacks(this::init); removeActiveDevice(false); @@ -1084,6 +1090,11 @@ public class LeAudioService extends ProfileService { return; } if (DBG) Log.d(TAG, "startBroadcast"); + + /* Start timeout to recover from stucked/error start Broadcast operation */ + mDialingOutTimeoutEvent = new DialingOutTimeoutEvent(); + mHandler.postDelayed(mDialingOutTimeoutEvent, DIALING_OUT_TIMEOUT_MS); + mLeAudioBroadcasterNativeInterface.startBroadcast(broadcastId); } @@ -1750,12 +1761,19 @@ public class LeAudioService extends ProfileService { * @param newDevice new supported broadcast audio device * @param previousDevice previous no longer supported broadcast audio device */ - /* TODO implement unicast overlap with connected unicast device */ private void updateBroadcastActiveDevice( BluetoothDevice newDevice, BluetoothDevice previousDevice, boolean suppressNoisyIntent) { mActiveBroadcastAudioDevice = newDevice; + if (DBG) { + Log.d( + TAG, + "updateBroadcastActiveDevice: newDevice: " + + newDevice + + ", previousDevice: " + + previousDevice); + } mAudioManager.handleBluetoothActiveDeviceChanged( newDevice, previousDevice, getBroadcastProfile(suppressNoisyIntent)); } @@ -2272,6 +2290,16 @@ public class LeAudioService extends ProfileService { || mBroadcastIdDeactivatedForUnicastTransition.isPresent())) { leaveConnectedInputDevice = true; newDirections |= AUDIO_DIRECTION_INPUT_BIT; + + /* Update Broadcast device before streaming state in handover case to avoid switch + * to non LE Audio device in Audio Manager e.g. Phone Speaker. + */ + BluetoothDevice device = + mAdapterService.getDeviceFromByte( + Utils.getBytesFromAddress("FF:FF:FF:FF:FF:FF")); + if (!device.equals(mActiveBroadcastAudioDevice)) { + updateBroadcastActiveDevice(device, mActiveBroadcastAudioDevice, true); + } } descriptor.mIsActive = false; @@ -2558,8 +2586,6 @@ public class LeAudioService extends ProfileService { updateFallbackUnicastGroupIdForBroadcast(LE_AUDIO_GROUP_ID_INVALID); updateBroadcastActiveDevice(null, mActiveBroadcastAudioDevice, false); return; - } else { - updateBroadcastActiveDevice(null, mActiveBroadcastAudioDevice, true); } if (DBG) { @@ -2573,6 +2599,21 @@ public class LeAudioService extends ProfileService { setActiveDevice(unicastDevice); } + void clearBroadcastTimeoutCallback() { + if (mHandler == null) { + Log.e(TAG, "No callback handler"); + return; + } + + /* Timeout callback already cleared */ + if (mDialingOutTimeoutEvent == null) { + return; + } + + mHandler.removeCallbacks(mDialingOutTimeoutEvent); + mDialingOutTimeoutEvent = null; + } + // Suppressed since this is part of a local process @SuppressLint("AndroidFrameworkRequiresPermission") void messageFromNative(LeAudioStackEvent stackEvent) { @@ -2786,6 +2827,11 @@ public class LeAudioService extends ProfileService { switch (groupStatus) { case LeAudioStackEvent.GROUP_STATUS_ACTIVE: { handleGroupTransitToActive(groupId); + + /* Clear possible exposed broadcast device after activating unicast */ + if (mActiveBroadcastAudioDevice != null) { + updateBroadcastActiveDevice(null, mActiveBroadcastAudioDevice, true); + } break; } case LeAudioStackEvent.GROUP_STATUS_INACTIVE: { @@ -2831,6 +2877,20 @@ public class LeAudioService extends ProfileService { } else { // TODO: Improve reason reporting or extend the native stack event with reason code + Log.e( + TAG, + "EVENT_TYPE_BROADCAST_CREATED: Failed to create broadcast: " + broadcastId); + + /* Disconnect Broadcast device which was connected to avoid non LE Audio sound + * leak in handover scenario. + */ + if ((mUnicastGroupIdDeactivatedForBroadcastTransition != LE_AUDIO_GROUP_ID_INVALID) + && mCreateBroadcastQueue.isEmpty() + && (!Objects.equals(device, mActiveBroadcastAudioDevice))) { + clearBroadcastTimeoutCallback(); + updateBroadcastActiveDevice(null, mActiveBroadcastAudioDevice, false); + } + notifyBroadcastStartFailed(broadcastId, BluetoothStatusCodes.ERROR_UNKNOWN); } @@ -2926,11 +2986,21 @@ public class LeAudioService extends ProfileService { bassClientService.suspendReceiversSourceSynchronization(broadcastId); } - // Notify audio manager - updateBroadcastActiveDevice(null, mActiveBroadcastAudioDevice, true); - /* Restore the Unicast stream from before the Broadcast was started. */ - transitionFromBroadcastToUnicast(); + if (mUnicastGroupIdDeactivatedForBroadcastTransition + != LE_AUDIO_GROUP_ID_INVALID) { + transitionFromBroadcastToUnicast(); + } else { + // Notify audio manager + if (mBroadcastDescriptors.values().stream() + .noneMatch( + d -> + d.mState.equals( + LeAudioStackEvent + .BROADCAST_STATE_STREAMING))) { + updateBroadcastActiveDevice(null, mActiveBroadcastAudioDevice, false); + } + } break; case LeAudioStackEvent.BROADCAST_STATE_STOPPING: if (DBG) Log.d(TAG, "Broadcast broadcastId: " + broadcastId + " stopping."); @@ -2942,6 +3012,8 @@ public class LeAudioService extends ProfileService { notifyPlaybackStarted(broadcastId, BluetoothStatusCodes.REASON_LOCAL_STACK_REQUEST); + clearBroadcastTimeoutCallback(); + if (previousState == LeAudioStackEvent.BROADCAST_STATE_PAUSED) { if (bassClientService != null) { bassClientService.resumeReceiversSourceSynchronization(); @@ -4250,6 +4322,24 @@ public class LeAudioService extends ProfileService { return audioFrameworkCalls; } + class DialingOutTimeoutEvent implements Runnable { + @Override + public void run() { + Log.w(TAG, "Failed to start Broadcast in time"); + + mDialingOutTimeoutEvent = null; + + if (getLeAudioService() == null) { + Log.e(TAG, "DialingOutTimeoutEvent: No LE Audio service"); + return; + } + + if (mActiveBroadcastAudioDevice != null) { + updateBroadcastActiveDevice(null, mActiveBroadcastAudioDevice, false); + } + } + } + /** * Binder object: must be a static class or memory leak may occur */ diff --git a/android/app/tests/unit/src/com/android/bluetooth/le_audio/LeAudioBroadcastServiceTest.java b/android/app/tests/unit/src/com/android/bluetooth/le_audio/LeAudioBroadcastServiceTest.java index 8525f7261df621e8697368124e151cbd9e555e9d..17057c9f3e931e5d4467f697ceadd38f6c251009 100644 --- a/android/app/tests/unit/src/com/android/bluetooth/le_audio/LeAudioBroadcastServiceTest.java +++ b/android/app/tests/unit/src/com/android/bluetooth/le_audio/LeAudioBroadcastServiceTest.java @@ -28,6 +28,7 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.media.AudioManager; +import android.media.BluetoothProfileConnectionInfo; import android.os.Looper; import android.os.ParcelUuid; import android.platform.test.flag.junit.SetFlagsRule; @@ -37,6 +38,7 @@ import androidx.test.filters.MediumTest; import androidx.test.runner.AndroidJUnit4; import com.android.bluetooth.TestUtils; +import com.android.bluetooth.Utils; import com.android.bluetooth.bass_client.BassClientService; import com.android.bluetooth.btservice.ActiveDeviceManager; import com.android.bluetooth.btservice.AdapterService; @@ -51,6 +53,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.mockito.Spy; @@ -67,6 +70,8 @@ public class LeAudioBroadcastServiceTest { private BluetoothAdapter mAdapter; private BluetoothDevice mDevice; + private BluetoothDevice mBroadcastDevice; + private Context mTargetContext; private LeAudioService mService; private LeAudioIntentReceiver mLeAudioIntentReceiver; @@ -228,7 +233,9 @@ public class LeAudioBroadcastServiceTest { mTargetContext.registerReceiver(mLeAudioIntentReceiver, filter); mDevice = TestUtils.getTestDevice(mAdapter, 0); - when(mLeAudioBroadcasterNativeInterface.getDevice(any(byte[].class))).thenReturn(mDevice); + mBroadcastDevice = TestUtils.getTestDevice(mAdapter, 1); + when(mAdapterService.getDeviceFromByte(Utils.getBytesFromAddress("FF:FF:FF:FF:FF:FF"))) + .thenReturn(mBroadcastDevice); mIntentQueue = new LinkedBlockingQueue<Intent>(); } @@ -794,12 +801,7 @@ public class LeAudioBroadcastServiceTest { mOnBroadcastStartFailedReason); } - @Test - public void testInCallDrivenBroadcastSwitch() { - int groupId = 1; - int broadcastId = 243; - byte[] code = {0x00, 0x01, 0x00, 0x02}; - + private void prepareHandoverStreamingBroadcast(int groupId, int broadcastId, byte[] code) { mSetFlagsRule.enableFlags(Flags.FLAG_AUDIO_ROUTING_CENTRALIZATION); mSetFlagsRule.enableFlags(Flags.FLAG_LEAUDIO_BROADCAST_AUDIO_HANDOVER_POLICIES); @@ -813,6 +815,14 @@ public class LeAudioBroadcastServiceTest { create_event.valueInt2 = LeAudioStackEvent.GROUP_STATUS_ACTIVE; mService.messageFromNative(create_event); + /* Verify Unicast input and output devices changed from null to mDevice */ + verify(mAudioManager, times(2)) + .handleBluetoothActiveDeviceChanged( + eq(mDevice), eq(null), any(BluetoothProfileConnectionInfo.class)); + Mockito.clearInvocations(mAudioManager); + + mService.notifyActiveDeviceChanged(mDevice); + /* Prepare create broadcast */ BluetoothLeAudioContentMetadata.Builder meta_builder = new BluetoothLeAudioContentMetadata.Builder(); @@ -823,6 +833,10 @@ public class LeAudioBroadcastServiceTest { BluetoothLeBroadcastSettings settings = buildBroadcastSettingsFromMetadata(meta, code, 1); mService.createBroadcast(settings); + verify(mAudioManager, times(1)) + .handleBluetoothActiveDeviceChanged( + eq(mBroadcastDevice), eq(null), any(BluetoothProfileConnectionInfo.class)); + /* Active group should become inactive */ int activeGroup = mService.getActiveGroupId(); Assert.assertEquals(activeGroup, LE_AUDIO_GROUP_ID_INVALID); @@ -833,6 +847,11 @@ public class LeAudioBroadcastServiceTest { create_event.valueInt2 = LeAudioStackEvent.GROUP_STATUS_INACTIVE; mService.messageFromNative(create_event); + /* Only one Unicast device should become inactive due to Sink monitor mode */ + verify(mAudioManager, times(1)) + .handleBluetoothActiveDeviceChanged( + eq(null), eq(mDevice), any(BluetoothProfileConnectionInfo.class)); + Mockito.clearInvocations(mAudioManager); List<BluetoothLeBroadcastSubgroupSettings> settingsList = settings.getSubgroupSettings(); int[] expectedQualityArray = @@ -850,6 +869,8 @@ public class LeAudioBroadcastServiceTest { eq(settings.getPublicBroadcastMetadata().getRawMetadata()), eq(expectedQualityArray), eq(expectedDataArray)); + verify(mLeAudioNativeInterface, times(1)) + .setUnicastMonitorMode(eq(LeAudioStackEvent.DIRECTION_SINK), eq(true)); activeGroup = mService.getActiveGroupId(); Assert.assertEquals(LE_AUDIO_GROUP_ID_INVALID, activeGroup); @@ -865,9 +886,19 @@ public class LeAudioBroadcastServiceTest { /* Switch to active streaming */ create_event = new LeAudioStackEvent(LeAudioStackEvent.EVENT_TYPE_BROADCAST_STATE); + create_event.device = mBroadcastDevice; create_event.valueInt1 = broadcastId; create_event.valueInt2 = LeAudioStackEvent.BROADCAST_STATE_STREAMING; mService.messageFromNative(create_event); + } + + @Test + public void testInCallDrivenBroadcastSwitch() { + int groupId = 1; + int broadcastId = 243; + byte[] code = {0x00, 0x01, 0x00, 0x02}; + + prepareHandoverStreamingBroadcast(groupId, broadcastId, code); /* Imitate setting device in call */ mService.setInCall(true); @@ -883,13 +914,21 @@ public class LeAudioBroadcastServiceTest { verify(mLeAudioNativeInterface, times(1)).setInCall(eq(true)); - create_event = new LeAudioStackEvent(LeAudioStackEvent.EVENT_TYPE_GROUP_STATUS_CHANGED); + LeAudioStackEvent create_event = + new LeAudioStackEvent(LeAudioStackEvent.EVENT_TYPE_GROUP_STATUS_CHANGED); create_event.valueInt1 = groupId; create_event.valueInt2 = LeAudioStackEvent.GROUP_STATUS_ACTIVE; mService.messageFromNative(create_event); + verify(mAudioManager, times(1)) + .handleBluetoothActiveDeviceChanged( + eq(mDevice), eq(null), any(BluetoothProfileConnectionInfo.class)); + verify(mAudioManager, times(1)) + .handleBluetoothActiveDeviceChanged( + eq(null), eq(mBroadcastDevice), any(BluetoothProfileConnectionInfo.class)); + /* Active group should become the one that was active before broadcasting */ - activeGroup = mService.getActiveGroupId(); + int activeGroup = mService.getActiveGroupId(); Assert.assertEquals(activeGroup, groupId); /* Imitate setting device not in call */ @@ -903,6 +942,14 @@ public class LeAudioBroadcastServiceTest { create_event.valueInt2 = LeAudioStackEvent.GROUP_STATUS_INACTIVE; mService.messageFromNative(create_event); + /* Only one Unicast device should become inactive due to Sink monitor mode */ + verify(mAudioManager, times(1)) + .handleBluetoothActiveDeviceChanged( + eq(null), eq(mDevice), any(BluetoothProfileConnectionInfo.class)); + verify(mAudioManager, times(1)) + .handleBluetoothActiveDeviceChanged( + eq(mBroadcastDevice), eq(null), any(BluetoothProfileConnectionInfo.class)); + /* Verify if broadcast is auto-started on start */ verify(mLeAudioBroadcasterNativeInterface, times(2)).startBroadcast(eq(broadcastId)); } @@ -913,82 +960,14 @@ public class LeAudioBroadcastServiceTest { int broadcastId = 243; byte[] code = {0x00, 0x01, 0x00, 0x02}; - mSetFlagsRule.enableFlags(Flags.FLAG_AUDIO_ROUTING_CENTRALIZATION); - mSetFlagsRule.enableFlags(Flags.FLAG_LEAUDIO_BROADCAST_AUDIO_HANDOVER_POLICIES); - - mService.mBroadcastCallbacks.register(mCallbacks); - - prepareConnectedUnicastDevice(groupId); - - LeAudioStackEvent create_event = - new LeAudioStackEvent(LeAudioStackEvent.EVENT_TYPE_GROUP_STATUS_CHANGED); - create_event.valueInt1 = groupId; - create_event.valueInt2 = LeAudioStackEvent.GROUP_STATUS_ACTIVE; - mService.messageFromNative(create_event); - - /* Prepare create broadcast */ - BluetoothLeAudioContentMetadata.Builder meta_builder = - new BluetoothLeAudioContentMetadata.Builder(); - meta_builder.setLanguage("ENG"); - meta_builder.setProgramInfo("Public broadcast info"); - BluetoothLeAudioContentMetadata meta = meta_builder.build(); - - BluetoothLeBroadcastSettings settings = buildBroadcastSettingsFromMetadata(meta, code, 1); - mService.createBroadcast(settings); - - /* Active group should become inactive */ - int activeGroup = mService.getActiveGroupId(); - Assert.assertEquals(activeGroup, LE_AUDIO_GROUP_ID_INVALID); - - /* Imitate group inactivity to cause create broadcast */ - create_event = new LeAudioStackEvent(LeAudioStackEvent.EVENT_TYPE_GROUP_STATUS_CHANGED); - create_event.valueInt1 = groupId; - create_event.valueInt2 = LeAudioStackEvent.GROUP_STATUS_INACTIVE; - mService.messageFromNative(create_event); - - 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)); - verify(mLeAudioNativeInterface, times(1)) - .setUnicastMonitorMode(eq(LeAudioStackEvent.DIRECTION_SINK),eq(true)); - - activeGroup = mService.getActiveGroupId(); - Assert.assertEquals(LE_AUDIO_GROUP_ID_INVALID, activeGroup); - - /* Check if broadcast is started automatically when created */ - 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)); - - /* Switch to active streaming */ - create_event = new LeAudioStackEvent(LeAudioStackEvent.EVENT_TYPE_BROADCAST_STATE); - create_event.valueInt1 = broadcastId; - create_event.valueInt2 = LeAudioStackEvent.BROADCAST_STATE_STREAMING; - mService.messageFromNative(create_event); + prepareHandoverStreamingBroadcast(groupId, broadcastId, code); /* Verify if broadcast is auto-started on start */ verify(mLeAudioBroadcasterNativeInterface, times(1)).startBroadcast(eq(broadcastId)); /* Imitate group change request by Bluetooth Sink HAL resume request */ - create_event = new LeAudioStackEvent(LeAudioStackEvent.EVENT_TYPE_UNICAST_MONITOR_MODE_STATUS); + LeAudioStackEvent create_event = + new LeAudioStackEvent(LeAudioStackEvent.EVENT_TYPE_UNICAST_MONITOR_MODE_STATUS); create_event.valueInt1 = LeAudioStackEvent.DIRECTION_SINK; create_event.valueInt2 = LeAudioStackEvent.STATUS_LOCAL_STREAM_REQUESTED; mService.messageFromNative(create_event); @@ -1007,8 +986,15 @@ public class LeAudioBroadcastServiceTest { create_event.valueInt2 = LeAudioStackEvent.GROUP_STATUS_ACTIVE; mService.messageFromNative(create_event); + verify(mAudioManager, times(1)) + .handleBluetoothActiveDeviceChanged( + eq(mDevice), eq(null), any(BluetoothProfileConnectionInfo.class)); + verify(mAudioManager, times(1)) + .handleBluetoothActiveDeviceChanged( + eq(null), eq(mBroadcastDevice), any(BluetoothProfileConnectionInfo.class)); + /* Active group should become the one that was active before broadcasting */ - activeGroup = mService.getActiveGroupId(); + int activeGroup = mService.getActiveGroupId(); Assert.assertEquals(activeGroup, groupId); /* Imitate group change request by Bluetooth Sink HAL suspend request */ @@ -1025,6 +1011,14 @@ public class LeAudioBroadcastServiceTest { create_event.valueInt2 = LeAudioStackEvent.GROUP_STATUS_INACTIVE; mService.messageFromNative(create_event); + /* Only one Unicast device should become inactive due to Sink monitor mode */ + verify(mAudioManager, times(1)) + .handleBluetoothActiveDeviceChanged( + eq(null), eq(mDevice), any(BluetoothProfileConnectionInfo.class)); + verify(mAudioManager, times(1)) + .handleBluetoothActiveDeviceChanged( + eq(mBroadcastDevice), eq(null), any(BluetoothProfileConnectionInfo.class)); + /* Verify if broadcast is auto-started on start */ verify(mLeAudioBroadcasterNativeInterface, times(2)).startBroadcast(eq(broadcastId)); }