Loading android/app/src/com/android/bluetooth/hfp/HeadsetService.java +59 −19 Original line number Diff line number Diff line Loading @@ -56,6 +56,16 @@ import java.util.Objects; /** * Provides Bluetooth Headset and Handsfree profile, as a service in the Bluetooth application. * * Four modes for SCO audio: * 1. Raw audio through {@link #connectAudio()} * 2. Telecom call through {@link #phoneStateChanged(int, int, int, String, int, boolean)} * 3. Virtual call through {@link #startScoUsingVirtualVoiceCall()} * 4. Voice recognition through {@link #startVoiceRecognition(BluetoothDevice)} * * When they happen at the same time, the order of preference is: * Raw audio > Telecom call > Virtual call > Voice Recognition * A higher preference mode will preempt lower preference mode */ public class HeadsetService extends ProfileService { private static final String TAG = "HeadsetService"; Loading Loading @@ -1147,7 +1157,7 @@ public class HeadsetService extends ProfileService { return false; } mVirtualCallStarted = true; // 3. Send virtual phone state changed to initialize SCO // Send virtual phone state changed to initialize SCO phoneStateChanged(0, 0, HeadsetHalConstants.CALL_STATE_DIALING, "", 0, true); phoneStateChanged(0, 0, HeadsetHalConstants.CALL_STATE_ALERTING, "", 0, true); phoneStateChanged(1, 0, HeadsetHalConstants.CALL_STATE_IDLE, "", 0, true); Loading Loading @@ -1203,6 +1213,10 @@ public class HeadsetService extends ProfileService { */ boolean dialOutgoingCall(BluetoothDevice fromDevice, String dialNumber) { synchronized (mStateMachines) { if (!isOnStateMachineThread()) { Log.e(TAG, "dialOutgoingCall must be called from state machine thread"); return false; } if (mDialingOutTimeoutEvent != null) { Log.e(TAG, "dialOutgoingCall, already dialing by " + mDialingOutTimeoutEvent); return false; Loading Loading @@ -1264,9 +1278,15 @@ public class HeadsetService extends ProfileService { } } mStateMachinesThread.getThreadHandler().post(() -> { boolean shouldCallAudioBeActiveBefore = shouldCallAudioBeActive(); mSystemInterface.getHeadsetPhoneState().setNumActiveCall(numActive); mSystemInterface.getHeadsetPhoneState().setNumHeldCall(numHeld); mSystemInterface.getHeadsetPhoneState().setCallState(callState); // Suspend A2DP when call about is about to become active if (callState != HeadsetHalConstants.CALL_STATE_DISCONNECTED && shouldCallAudioBeActive() && !shouldCallAudioBeActiveBefore) { mSystemInterface.getAudioManager().setParameters("A2dpSuspended=true"); } }); doForEachConnectedStateMachine( stateMachine -> stateMachine.sendMessage(HeadsetStateMachine.CALL_STATE_CHANGED, Loading Loading @@ -1338,8 +1358,8 @@ public class HeadsetService extends ProfileService { } MetricsLogger.logProfileConnectionEvent(BluetoothMetricsProto.ProfileId.HEADSET); } if (fromState == BluetoothProfile.STATE_CONNECTED && toState != BluetoothProfile.STATE_CONNECTED) { if (fromState != BluetoothProfile.STATE_DISCONNECTED && toState == BluetoothProfile.STATE_DISCONNECTED) { if (audioConnectableDevices.size() <= 1) { mInbandRingingRuntimeDisable = false; doForEachConnectedStateMachine( Loading @@ -1353,9 +1373,22 @@ public class HeadsetService extends ProfileService { } } private boolean shouldCallAudioBeActive() { return mSystemInterface.isInCall() || (mSystemInterface.isRinging() && isInbandRingingEnabled()); } /** * Only persist audio during active device switch when call audio is supposed to be active and * virtual call has not been started. Virtual call is ignored because AudioService and * applications should reconnect SCO during active device switch and forcing SCO connection * here will make AudioService think SCO is started externally instead of by one of its SCO * clients. * * @return true if call audio should be active and no virtual call is going on */ private boolean shouldPersistAudio() { return (!mVirtualCallStarted && mSystemInterface.isInCall()) || ( mSystemInterface.isRinging() && isInbandRingingEnabled()); return !mVirtualCallStarted && shouldCallAudioBeActive(); } /** Loading @@ -1370,20 +1403,21 @@ public class HeadsetService extends ProfileService { public void onAudioStateChangedFromStateMachine(BluetoothDevice device, int fromState, int toState) { synchronized (mStateMachines) { if (fromState != BluetoothHeadset.STATE_AUDIO_CONNECTED && toState == BluetoothHeadset.STATE_AUDIO_CONNECTED) { // Do nothing } if (fromState != BluetoothHeadset.STATE_AUDIO_DISCONNECTED && toState == BluetoothHeadset.STATE_AUDIO_DISCONNECTED) { if (toState == BluetoothHeadset.STATE_AUDIO_DISCONNECTED) { if (fromState != BluetoothHeadset.STATE_AUDIO_DISCONNECTED) { if (mActiveDevice != null && !mActiveDevice.equals(device) && shouldPersistAudio()) { if (!connectAudio(mActiveDevice)) { Log.w(TAG, "onAudioStateChangedFromStateMachine, failed to connect to new " + "active device " + mActiveDevice + ", after " + device + " is disconnected from SCO"); Log.w(TAG, "onAudioStateChangedFromStateMachine, failed to connect" + " audio to new " + "active device " + mActiveDevice + ", after " + device + " is disconnected from SCO"); } } } // Unsuspend A2DP when SCO connection is gone and call state is idle if (mSystemInterface.isCallIdle()) { mSystemInterface.getAudioManager().setParameters("A2dpSuspended=false"); } } } } Loading Loading @@ -1460,10 +1494,10 @@ public class HeadsetService extends ProfileService { Log.w(TAG, "isScoAcceptable: rejected SCO since audio route is not allowed"); return false; } if (mSystemInterface.isInCall() || mVoiceRecognitionStarted || mVirtualCallStarted) { if (mVoiceRecognitionStarted || mVirtualCallStarted) { return true; } if (mSystemInterface.isRinging() && isInbandRingingEnabled()) { if (shouldCallAudioBeActive()) { return true; } Log.w(TAG, "isScoAcceptable: rejected SCO, inCall=" + mSystemInterface.isInCall() Loading Loading @@ -1492,6 +1526,12 @@ public class HeadsetService extends ProfileService { } } private boolean isOnStateMachineThread() { final Looper myLooper = Looper.myLooper(); return myLooper != null && (mStateMachinesThread != null) && (myLooper.getThread().getId() == mStateMachinesThread.getId()); } @Override public void dump(StringBuilder sb) { synchronized (mStateMachines) { Loading android/app/src/com/android/bluetooth/hfp/HeadsetStateMachine.java +11 −3 Original line number Diff line number Diff line Loading @@ -1067,7 +1067,9 @@ public class HeadsetStateMachine extends StateMachine { break; case CONNECT_AUDIO: stateLogD("CONNECT_AUDIO, device=" + mDevice); mSystemInterface.getAudioManager().setParameters("A2dpSuspended=true"); if (!mNativeInterface.connectAudio(mDevice)) { mSystemInterface.getAudioManager().setParameters("A2dpSuspended=false"); stateLogE("Failed to connect SCO audio for " + mDevice); // No state change involved, fire broadcast immediately broadcastAudioState(mDevice, BluetoothHeadset.STATE_AUDIO_DISCONNECTED, Loading Loading @@ -1510,7 +1512,10 @@ public class HeadsetStateMachine extends StateMachine { // Whereas for VoiceDial we want to activate the SCO connection but we are still // in MODE_NORMAL and hence the need to explicitly suspend the A2DP stream mSystemInterface.getAudioManager().setParameters("A2dpSuspended=true"); mNativeInterface.connectAudio(mDevice); if (!mNativeInterface.connectAudio(mDevice)) { mSystemInterface.getAudioManager().setParameters("A2dpSuspended=false"); Log.w(TAG, "processLocalVrEvent: failed connectAudio to " + mDevice); } } if (mSystemInterface.getVoiceRecognitionWakeLock().isHeld()) { Loading @@ -1527,7 +1532,6 @@ public class HeadsetStateMachine extends StateMachine { if (mNativeInterface.stopVoiceRecognition(mDevice) && !mSystemInterface.isInCall() && getAudioState() != BluetoothHeadset.STATE_AUDIO_DISCONNECTED) { mNativeInterface.disconnectAudio(mDevice); mSystemInterface.getAudioManager().setParameters("A2dpSuspended=false"); } } } Loading Loading @@ -1939,7 +1943,11 @@ public class HeadsetStateMachine extends StateMachine { } else if (phoneState.getNumActiveCall() > 0) { if (getAudioState() != BluetoothHeadset.STATE_AUDIO_DISCONNECTED) { mHeadsetService.setActiveDevice(mDevice); mNativeInterface.connectAudio(mDevice); mSystemInterface.getAudioManager().setParameters("A2dpSuspended=true"); if (!mNativeInterface.connectAudio(mDevice)) { mSystemInterface.getAudioManager().setParameters("A2dpSuspended=false"); Log.w(TAG, "processKeyPressed: failed to connectAudio to " + mDevice); } } else { mSystemInterface.hangupCall(device); } Loading android/app/src/com/android/bluetooth/hfp/HeadsetSystemInterface.java +10 −0 Original line number Diff line number Diff line Loading @@ -342,4 +342,14 @@ public class HeadsetSystemInterface { return mHeadsetPhoneState.getCallState() == HeadsetHalConstants.CALL_STATE_INCOMING; } /** * Check if call status is idle * * @return true if call state is neither ringing nor in call */ @VisibleForTesting public boolean isCallIdle() { return !isInCall() && !isRinging(); } } Loading
android/app/src/com/android/bluetooth/hfp/HeadsetService.java +59 −19 Original line number Diff line number Diff line Loading @@ -56,6 +56,16 @@ import java.util.Objects; /** * Provides Bluetooth Headset and Handsfree profile, as a service in the Bluetooth application. * * Four modes for SCO audio: * 1. Raw audio through {@link #connectAudio()} * 2. Telecom call through {@link #phoneStateChanged(int, int, int, String, int, boolean)} * 3. Virtual call through {@link #startScoUsingVirtualVoiceCall()} * 4. Voice recognition through {@link #startVoiceRecognition(BluetoothDevice)} * * When they happen at the same time, the order of preference is: * Raw audio > Telecom call > Virtual call > Voice Recognition * A higher preference mode will preempt lower preference mode */ public class HeadsetService extends ProfileService { private static final String TAG = "HeadsetService"; Loading Loading @@ -1147,7 +1157,7 @@ public class HeadsetService extends ProfileService { return false; } mVirtualCallStarted = true; // 3. Send virtual phone state changed to initialize SCO // Send virtual phone state changed to initialize SCO phoneStateChanged(0, 0, HeadsetHalConstants.CALL_STATE_DIALING, "", 0, true); phoneStateChanged(0, 0, HeadsetHalConstants.CALL_STATE_ALERTING, "", 0, true); phoneStateChanged(1, 0, HeadsetHalConstants.CALL_STATE_IDLE, "", 0, true); Loading Loading @@ -1203,6 +1213,10 @@ public class HeadsetService extends ProfileService { */ boolean dialOutgoingCall(BluetoothDevice fromDevice, String dialNumber) { synchronized (mStateMachines) { if (!isOnStateMachineThread()) { Log.e(TAG, "dialOutgoingCall must be called from state machine thread"); return false; } if (mDialingOutTimeoutEvent != null) { Log.e(TAG, "dialOutgoingCall, already dialing by " + mDialingOutTimeoutEvent); return false; Loading Loading @@ -1264,9 +1278,15 @@ public class HeadsetService extends ProfileService { } } mStateMachinesThread.getThreadHandler().post(() -> { boolean shouldCallAudioBeActiveBefore = shouldCallAudioBeActive(); mSystemInterface.getHeadsetPhoneState().setNumActiveCall(numActive); mSystemInterface.getHeadsetPhoneState().setNumHeldCall(numHeld); mSystemInterface.getHeadsetPhoneState().setCallState(callState); // Suspend A2DP when call about is about to become active if (callState != HeadsetHalConstants.CALL_STATE_DISCONNECTED && shouldCallAudioBeActive() && !shouldCallAudioBeActiveBefore) { mSystemInterface.getAudioManager().setParameters("A2dpSuspended=true"); } }); doForEachConnectedStateMachine( stateMachine -> stateMachine.sendMessage(HeadsetStateMachine.CALL_STATE_CHANGED, Loading Loading @@ -1338,8 +1358,8 @@ public class HeadsetService extends ProfileService { } MetricsLogger.logProfileConnectionEvent(BluetoothMetricsProto.ProfileId.HEADSET); } if (fromState == BluetoothProfile.STATE_CONNECTED && toState != BluetoothProfile.STATE_CONNECTED) { if (fromState != BluetoothProfile.STATE_DISCONNECTED && toState == BluetoothProfile.STATE_DISCONNECTED) { if (audioConnectableDevices.size() <= 1) { mInbandRingingRuntimeDisable = false; doForEachConnectedStateMachine( Loading @@ -1353,9 +1373,22 @@ public class HeadsetService extends ProfileService { } } private boolean shouldCallAudioBeActive() { return mSystemInterface.isInCall() || (mSystemInterface.isRinging() && isInbandRingingEnabled()); } /** * Only persist audio during active device switch when call audio is supposed to be active and * virtual call has not been started. Virtual call is ignored because AudioService and * applications should reconnect SCO during active device switch and forcing SCO connection * here will make AudioService think SCO is started externally instead of by one of its SCO * clients. * * @return true if call audio should be active and no virtual call is going on */ private boolean shouldPersistAudio() { return (!mVirtualCallStarted && mSystemInterface.isInCall()) || ( mSystemInterface.isRinging() && isInbandRingingEnabled()); return !mVirtualCallStarted && shouldCallAudioBeActive(); } /** Loading @@ -1370,20 +1403,21 @@ public class HeadsetService extends ProfileService { public void onAudioStateChangedFromStateMachine(BluetoothDevice device, int fromState, int toState) { synchronized (mStateMachines) { if (fromState != BluetoothHeadset.STATE_AUDIO_CONNECTED && toState == BluetoothHeadset.STATE_AUDIO_CONNECTED) { // Do nothing } if (fromState != BluetoothHeadset.STATE_AUDIO_DISCONNECTED && toState == BluetoothHeadset.STATE_AUDIO_DISCONNECTED) { if (toState == BluetoothHeadset.STATE_AUDIO_DISCONNECTED) { if (fromState != BluetoothHeadset.STATE_AUDIO_DISCONNECTED) { if (mActiveDevice != null && !mActiveDevice.equals(device) && shouldPersistAudio()) { if (!connectAudio(mActiveDevice)) { Log.w(TAG, "onAudioStateChangedFromStateMachine, failed to connect to new " + "active device " + mActiveDevice + ", after " + device + " is disconnected from SCO"); Log.w(TAG, "onAudioStateChangedFromStateMachine, failed to connect" + " audio to new " + "active device " + mActiveDevice + ", after " + device + " is disconnected from SCO"); } } } // Unsuspend A2DP when SCO connection is gone and call state is idle if (mSystemInterface.isCallIdle()) { mSystemInterface.getAudioManager().setParameters("A2dpSuspended=false"); } } } } Loading Loading @@ -1460,10 +1494,10 @@ public class HeadsetService extends ProfileService { Log.w(TAG, "isScoAcceptable: rejected SCO since audio route is not allowed"); return false; } if (mSystemInterface.isInCall() || mVoiceRecognitionStarted || mVirtualCallStarted) { if (mVoiceRecognitionStarted || mVirtualCallStarted) { return true; } if (mSystemInterface.isRinging() && isInbandRingingEnabled()) { if (shouldCallAudioBeActive()) { return true; } Log.w(TAG, "isScoAcceptable: rejected SCO, inCall=" + mSystemInterface.isInCall() Loading Loading @@ -1492,6 +1526,12 @@ public class HeadsetService extends ProfileService { } } private boolean isOnStateMachineThread() { final Looper myLooper = Looper.myLooper(); return myLooper != null && (mStateMachinesThread != null) && (myLooper.getThread().getId() == mStateMachinesThread.getId()); } @Override public void dump(StringBuilder sb) { synchronized (mStateMachines) { Loading
android/app/src/com/android/bluetooth/hfp/HeadsetStateMachine.java +11 −3 Original line number Diff line number Diff line Loading @@ -1067,7 +1067,9 @@ public class HeadsetStateMachine extends StateMachine { break; case CONNECT_AUDIO: stateLogD("CONNECT_AUDIO, device=" + mDevice); mSystemInterface.getAudioManager().setParameters("A2dpSuspended=true"); if (!mNativeInterface.connectAudio(mDevice)) { mSystemInterface.getAudioManager().setParameters("A2dpSuspended=false"); stateLogE("Failed to connect SCO audio for " + mDevice); // No state change involved, fire broadcast immediately broadcastAudioState(mDevice, BluetoothHeadset.STATE_AUDIO_DISCONNECTED, Loading Loading @@ -1510,7 +1512,10 @@ public class HeadsetStateMachine extends StateMachine { // Whereas for VoiceDial we want to activate the SCO connection but we are still // in MODE_NORMAL and hence the need to explicitly suspend the A2DP stream mSystemInterface.getAudioManager().setParameters("A2dpSuspended=true"); mNativeInterface.connectAudio(mDevice); if (!mNativeInterface.connectAudio(mDevice)) { mSystemInterface.getAudioManager().setParameters("A2dpSuspended=false"); Log.w(TAG, "processLocalVrEvent: failed connectAudio to " + mDevice); } } if (mSystemInterface.getVoiceRecognitionWakeLock().isHeld()) { Loading @@ -1527,7 +1532,6 @@ public class HeadsetStateMachine extends StateMachine { if (mNativeInterface.stopVoiceRecognition(mDevice) && !mSystemInterface.isInCall() && getAudioState() != BluetoothHeadset.STATE_AUDIO_DISCONNECTED) { mNativeInterface.disconnectAudio(mDevice); mSystemInterface.getAudioManager().setParameters("A2dpSuspended=false"); } } } Loading Loading @@ -1939,7 +1943,11 @@ public class HeadsetStateMachine extends StateMachine { } else if (phoneState.getNumActiveCall() > 0) { if (getAudioState() != BluetoothHeadset.STATE_AUDIO_DISCONNECTED) { mHeadsetService.setActiveDevice(mDevice); mNativeInterface.connectAudio(mDevice); mSystemInterface.getAudioManager().setParameters("A2dpSuspended=true"); if (!mNativeInterface.connectAudio(mDevice)) { mSystemInterface.getAudioManager().setParameters("A2dpSuspended=false"); Log.w(TAG, "processKeyPressed: failed to connectAudio to " + mDevice); } } else { mSystemInterface.hangupCall(device); } Loading
android/app/src/com/android/bluetooth/hfp/HeadsetSystemInterface.java +10 −0 Original line number Diff line number Diff line Loading @@ -342,4 +342,14 @@ public class HeadsetSystemInterface { return mHeadsetPhoneState.getCallState() == HeadsetHalConstants.CALL_STATE_INCOMING; } /** * Check if call status is idle * * @return true if call state is neither ringing nor in call */ @VisibleForTesting public boolean isCallIdle() { return !isInCall() && !isRinging(); } }