Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 27fca676 authored by Jack He's avatar Jack He
Browse files

SCO: Suspend and resume A2DP before SCO setup and after SCO tear down

* Suspend A2DP:
  - before calling mNativeInterface.connectAudio(device)
  - before call state becomes busy and audio should be connected, as defined by:
    1. numActive + numHeld > 0
    2. Call setup state is not idle, but not incoming
    3. Call state is incoming and in-band ringing is enabled
* Resume A2DP:
  - when mNativeInterface.connectAudio(device) returns false
  - when SCO audio is disconnected and call state is idle as defined by:
    1. numActive + numHeld > 0
    2. Call setup state is not idle

Bug: 74988740
Test: make a hangout call while music is playing
Change-Id: I85672b4e61c91a7db2458600ad3bacfe8d529af1
(cherry picked from commit 47fa7f7b)
parent bd2375e7
Loading
Loading
Loading
Loading
+59 −19
Original line number Diff line number Diff line
@@ -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";
@@ -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);
@@ -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;
@@ -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,
@@ -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(
@@ -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();
    }

    /**
@@ -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");
                }
            }
        }
    }
@@ -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()
@@ -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) {
+11 −3
Original line number Diff line number Diff line
@@ -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,
@@ -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()) {
@@ -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");
                }
            }
        }
@@ -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);
            }
+10 −0
Original line number Diff line number Diff line
@@ -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();
    }

}