Loading android/app/src/com/android/bluetooth/hfp/HeadsetService.java +40 −1 Original line number Diff line number Diff line Loading @@ -24,6 +24,7 @@ import static com.android.modules.utils.build.SdkLevel.isAtLeastU; import static java.util.Objects.requireNonNull; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.bluetooth.BluetoothClass; Loading Loading @@ -2130,6 +2131,26 @@ public class HeadsetService extends ProfileService { } } /** * Check if the device only allows HFP profile as audio profile * * @param device Bluetooth device * @return true if it is a BluetoothDevice with only HFP profile connectable */ private boolean isHFPAudioOnly(@NonNull BluetoothDevice device) { int hfpPolicy = mDatabaseManager.getProfileConnectionPolicy(device, BluetoothProfile.HEADSET); int a2dpPolicy = mDatabaseManager.getProfileConnectionPolicy(device, BluetoothProfile.A2DP); int leAudioPolicy = mDatabaseManager.getProfileConnectionPolicy(device, BluetoothProfile.LE_AUDIO); int ashaPolicy = mDatabaseManager.getProfileConnectionPolicy(device, BluetoothProfile.HEARING_AID); return hfpPolicy == BluetoothProfile.CONNECTION_POLICY_ALLOWED && a2dpPolicy != BluetoothProfile.CONNECTION_POLICY_ALLOWED && leAudioPolicy != BluetoothProfile.CONNECTION_POLICY_ALLOWED && ashaPolicy != BluetoothProfile.CONNECTION_POLICY_ALLOWED; } private boolean shouldCallAudioBeActive() { return mSystemInterface.isInCall() || (mSystemInterface.isRinging() && isInbandRingingEnabled()); Loading Loading @@ -2191,12 +2212,30 @@ public class HeadsetService extends ProfileService { // co-existence see {@link LeAudioService#setInactiveForHfpHandover} if (Flags.leaudioResumeActiveAfterHfpHandover()) { LeAudioService leAudioService = mFactory.getLeAudioService(); if (!Flags.keepHfpActiveDuringLeaudioHandover() && leAudioService != null && !leAudioService.getConnectedDevices().isEmpty() && leAudioService.getActiveDevices().get(0) == null) { leAudioService.setActiveAfterHfpHandover(); } // usually controller limitation cause CONNECTING -> DISCONNECTED, so only // resume LE audio active device if it is HFP audio only and SCO disconnected Log.v( TAG, "keep HFP active during handover: isHFPAudioOnly=" + isHFPAudioOnly(device)); if (Flags.keepHfpActiveDuringLeaudioHandover() && fromState != BluetoothHeadset.STATE_AUDIO_CONNECTING && isHFPAudioOnly(device)) { if (leAudioService != null && !leAudioService.getConnectedDevices().isEmpty() && leAudioService.getActiveDevices().get(0) == null) { leAudioService.setActiveAfterHfpHandover(); } } } // Unsuspend A2DP when SCO connection is gone and call state is idle if (mSystemInterface.isCallIdle()) { Loading android/app/tests/unit/src/com/android/bluetooth/hfp/HeadsetServiceAndStateMachineTest.java +48 −3 Original line number Diff line number Diff line Loading @@ -1577,18 +1577,19 @@ public class HeadsetServiceAndStateMachineTest { } @Test public void testHfpHandoverToLeAudioAfterScoDisconnect() { public void testHfpOnlyHandoverToLeAudioAfterScoDisconnect() { BluetoothDevice device = TestUtils.getTestDevice(mAdapter, 0); mSetFlagsRule.enableFlags(Flags.FLAG_LEAUDIO_RESUME_ACTIVE_AFTER_HFP_HANDOVER); mSetFlagsRule.enableFlags(Flags.FLAG_KEEP_HFP_ACTIVE_DURING_LEAUDIO_HANDOVER); assertThat(mHeadsetService.mFactory).isNotNull(); mHeadsetService.mFactory = mServiceFactory; doReturn(mLeAudioService).when(mServiceFactory).getLeAudioService(); doReturn(List.of(device)).when(mLeAudioService).getConnectedDevices(); List<BluetoothDevice> activeDeviceList = new ArrayList<>(); activeDeviceList.add(null); doReturn(activeDeviceList).when(mLeAudioService).getActiveDevices(); mHeadsetService.mFactory = mServiceFactory; doReturn(true).when(mSystemInterface).isCallIdle(); // Connect HF connectTestDevice(device); Loading @@ -1599,12 +1600,56 @@ public class HeadsetServiceAndStateMachineTest { assertThat(mHeadsetService.getActiveDevice()).isEqualTo(device); verify(mNativeInterface).sendBsir(eq(device), eq(true)); // this device is a HFP only device doReturn(BluetoothProfile.CONNECTION_POLICY_ALLOWED) .when(mDatabaseManager) .getProfileConnectionPolicy(device, BluetoothProfile.HEADSET); doReturn(BluetoothProfile.CONNECTION_POLICY_UNKNOWN) .when(mDatabaseManager) .getProfileConnectionPolicy(device, BluetoothProfile.A2DP); doReturn(BluetoothProfile.CONNECTION_POLICY_UNKNOWN) .when(mDatabaseManager) .getProfileConnectionPolicy(device, BluetoothProfile.HEARING_AID); doReturn(BluetoothProfile.CONNECTION_POLICY_UNKNOWN) .when(mDatabaseManager) .getProfileConnectionPolicy(device, BluetoothProfile.LE_AUDIO); doReturn(true).when(mSystemInterface).isInCall(); mHeadsetService.messageFromNative( new HeadsetStackEvent( HeadsetStackEvent.EVENT_TYPE_AUDIO_STATE_CHANGED, HeadsetHalConstants.AUDIO_STATE_CONNECTING, device)); mTestLooper.dispatchAll(); // simulate controller cannot handle SCO and CIS coexistence, // and SCO is failed to connect initially, mHeadsetService.messageFromNative( new HeadsetStackEvent( HeadsetStackEvent.EVENT_TYPE_AUDIO_STATE_CHANGED, HeadsetHalConstants.AUDIO_STATE_DISCONNECTED, device)); mTestLooper.dispatchAll(); // at this moment, should not resume LE Audio active device verify(mLeAudioService, never()).setActiveAfterHfpHandover(); mHeadsetService.messageFromNative( new HeadsetStackEvent( HeadsetStackEvent.EVENT_TYPE_AUDIO_STATE_CHANGED, HeadsetHalConstants.AUDIO_STATE_CONNECTING, device)); mTestLooper.dispatchAll(); // then SCO is connected mHeadsetService.messageFromNative( new HeadsetStackEvent( HeadsetStackEvent.EVENT_TYPE_AUDIO_STATE_CHANGED, HeadsetHalConstants.AUDIO_STATE_CONNECTED, device)); doReturn(false).when(mSystemInterface).isInCall(); doReturn(true).when(mSystemInterface).isCallIdle(); // Audio disconnected mHeadsetService.messageFromNative( new HeadsetStackEvent( Loading Loading
android/app/src/com/android/bluetooth/hfp/HeadsetService.java +40 −1 Original line number Diff line number Diff line Loading @@ -24,6 +24,7 @@ import static com.android.modules.utils.build.SdkLevel.isAtLeastU; import static java.util.Objects.requireNonNull; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.bluetooth.BluetoothClass; Loading Loading @@ -2130,6 +2131,26 @@ public class HeadsetService extends ProfileService { } } /** * Check if the device only allows HFP profile as audio profile * * @param device Bluetooth device * @return true if it is a BluetoothDevice with only HFP profile connectable */ private boolean isHFPAudioOnly(@NonNull BluetoothDevice device) { int hfpPolicy = mDatabaseManager.getProfileConnectionPolicy(device, BluetoothProfile.HEADSET); int a2dpPolicy = mDatabaseManager.getProfileConnectionPolicy(device, BluetoothProfile.A2DP); int leAudioPolicy = mDatabaseManager.getProfileConnectionPolicy(device, BluetoothProfile.LE_AUDIO); int ashaPolicy = mDatabaseManager.getProfileConnectionPolicy(device, BluetoothProfile.HEARING_AID); return hfpPolicy == BluetoothProfile.CONNECTION_POLICY_ALLOWED && a2dpPolicy != BluetoothProfile.CONNECTION_POLICY_ALLOWED && leAudioPolicy != BluetoothProfile.CONNECTION_POLICY_ALLOWED && ashaPolicy != BluetoothProfile.CONNECTION_POLICY_ALLOWED; } private boolean shouldCallAudioBeActive() { return mSystemInterface.isInCall() || (mSystemInterface.isRinging() && isInbandRingingEnabled()); Loading Loading @@ -2191,12 +2212,30 @@ public class HeadsetService extends ProfileService { // co-existence see {@link LeAudioService#setInactiveForHfpHandover} if (Flags.leaudioResumeActiveAfterHfpHandover()) { LeAudioService leAudioService = mFactory.getLeAudioService(); if (!Flags.keepHfpActiveDuringLeaudioHandover() && leAudioService != null && !leAudioService.getConnectedDevices().isEmpty() && leAudioService.getActiveDevices().get(0) == null) { leAudioService.setActiveAfterHfpHandover(); } // usually controller limitation cause CONNECTING -> DISCONNECTED, so only // resume LE audio active device if it is HFP audio only and SCO disconnected Log.v( TAG, "keep HFP active during handover: isHFPAudioOnly=" + isHFPAudioOnly(device)); if (Flags.keepHfpActiveDuringLeaudioHandover() && fromState != BluetoothHeadset.STATE_AUDIO_CONNECTING && isHFPAudioOnly(device)) { if (leAudioService != null && !leAudioService.getConnectedDevices().isEmpty() && leAudioService.getActiveDevices().get(0) == null) { leAudioService.setActiveAfterHfpHandover(); } } } // Unsuspend A2DP when SCO connection is gone and call state is idle if (mSystemInterface.isCallIdle()) { Loading
android/app/tests/unit/src/com/android/bluetooth/hfp/HeadsetServiceAndStateMachineTest.java +48 −3 Original line number Diff line number Diff line Loading @@ -1577,18 +1577,19 @@ public class HeadsetServiceAndStateMachineTest { } @Test public void testHfpHandoverToLeAudioAfterScoDisconnect() { public void testHfpOnlyHandoverToLeAudioAfterScoDisconnect() { BluetoothDevice device = TestUtils.getTestDevice(mAdapter, 0); mSetFlagsRule.enableFlags(Flags.FLAG_LEAUDIO_RESUME_ACTIVE_AFTER_HFP_HANDOVER); mSetFlagsRule.enableFlags(Flags.FLAG_KEEP_HFP_ACTIVE_DURING_LEAUDIO_HANDOVER); assertThat(mHeadsetService.mFactory).isNotNull(); mHeadsetService.mFactory = mServiceFactory; doReturn(mLeAudioService).when(mServiceFactory).getLeAudioService(); doReturn(List.of(device)).when(mLeAudioService).getConnectedDevices(); List<BluetoothDevice> activeDeviceList = new ArrayList<>(); activeDeviceList.add(null); doReturn(activeDeviceList).when(mLeAudioService).getActiveDevices(); mHeadsetService.mFactory = mServiceFactory; doReturn(true).when(mSystemInterface).isCallIdle(); // Connect HF connectTestDevice(device); Loading @@ -1599,12 +1600,56 @@ public class HeadsetServiceAndStateMachineTest { assertThat(mHeadsetService.getActiveDevice()).isEqualTo(device); verify(mNativeInterface).sendBsir(eq(device), eq(true)); // this device is a HFP only device doReturn(BluetoothProfile.CONNECTION_POLICY_ALLOWED) .when(mDatabaseManager) .getProfileConnectionPolicy(device, BluetoothProfile.HEADSET); doReturn(BluetoothProfile.CONNECTION_POLICY_UNKNOWN) .when(mDatabaseManager) .getProfileConnectionPolicy(device, BluetoothProfile.A2DP); doReturn(BluetoothProfile.CONNECTION_POLICY_UNKNOWN) .when(mDatabaseManager) .getProfileConnectionPolicy(device, BluetoothProfile.HEARING_AID); doReturn(BluetoothProfile.CONNECTION_POLICY_UNKNOWN) .when(mDatabaseManager) .getProfileConnectionPolicy(device, BluetoothProfile.LE_AUDIO); doReturn(true).when(mSystemInterface).isInCall(); mHeadsetService.messageFromNative( new HeadsetStackEvent( HeadsetStackEvent.EVENT_TYPE_AUDIO_STATE_CHANGED, HeadsetHalConstants.AUDIO_STATE_CONNECTING, device)); mTestLooper.dispatchAll(); // simulate controller cannot handle SCO and CIS coexistence, // and SCO is failed to connect initially, mHeadsetService.messageFromNative( new HeadsetStackEvent( HeadsetStackEvent.EVENT_TYPE_AUDIO_STATE_CHANGED, HeadsetHalConstants.AUDIO_STATE_DISCONNECTED, device)); mTestLooper.dispatchAll(); // at this moment, should not resume LE Audio active device verify(mLeAudioService, never()).setActiveAfterHfpHandover(); mHeadsetService.messageFromNative( new HeadsetStackEvent( HeadsetStackEvent.EVENT_TYPE_AUDIO_STATE_CHANGED, HeadsetHalConstants.AUDIO_STATE_CONNECTING, device)); mTestLooper.dispatchAll(); // then SCO is connected mHeadsetService.messageFromNative( new HeadsetStackEvent( HeadsetStackEvent.EVENT_TYPE_AUDIO_STATE_CHANGED, HeadsetHalConstants.AUDIO_STATE_CONNECTED, device)); doReturn(false).when(mSystemInterface).isInCall(); doReturn(true).when(mSystemInterface).isCallIdle(); // Audio disconnected mHeadsetService.messageFromNative( new HeadsetStackEvent( Loading