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

Commit a05462e4 authored by Jack He's avatar Jack He
Browse files

HFP: Only remove state machine when device is unbonded and disconnected

SUMMARY:
* Remove a state machine only when a device is both unbonded and
  disconnected
* Add HeadsetServiceAndStateMachineTest to verify this behavior
* Reject API connect() attempt when device does not have headset UUIDs

DETAILS:
* A state machine is no longer useful if the associated device is no
  longer bonded. However, sometimes the device may get disconnected
  after it is unbonded. Therefore, we should only remove a state machine
  when the device is both unbonded and disconnected.
* Also, we should only allow an HeadsetService.connect() API call when
  there is at least one headset UUID present.
* HeadsetServiceAndStateMachineTest is added to integerate both
  HeadsetService and HeadsetStateMachine to complete a set of
  semi-integration tests. This new set of tests allows us to verify
  whether a connect() API call will eventually trigger a connectHfp() to
  the native interface, utilizing all logic in the middle.
* However, we do need to assume certain timing condition in such
  semi-integration test. 250ms is set as a limit for any state machine
  based message handling, which should be enough for most modern day
  Android devices.
* We have to use AdapterService instead of BluetoothDevice to get bond
  state, device name, and so on since we want to be able to mock these
  methods and BluetoothDevice class is final.
* Thus, this CL also adds a new parameter to HeadsetStateMachine during
  construction so that it has a reference to AdapterService suppplied
  through its constructor. This is to make sure that entire HFP stack
  uses the same reference of AdapterService obtained by HeadsetService.

Bug: 72529611
Test: disconnect and then quickly unpair device
Change-Id: I49ec70d60e257ffd4484e536bdb66d6da7b3b377
parent b0e640c4
Loading
Loading
Loading
Loading
+23 −2
Original line number Diff line number Diff line
@@ -1800,6 +1800,11 @@ public class AdapterService extends Service {
        return mAdapterProperties.discoveryEndMillis();
    }

    /**
     * Same as API method {@link BluetoothAdapter#getBondedDevices()}
     *
     * @return array of bonded {@link BluetoothDevice} or null on error
     */
    public BluetoothDevice[] getBondedDevices() {
        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
        return mAdapterProperties.getBondedDevices();
@@ -1895,7 +1900,17 @@ public class AdapterService extends Service {
        return true;
    }

    int getBondState(BluetoothDevice device) {
    /**
     * Get the bond state of a particular {@link BluetoothDevice}
     *
     * @param device remote device of interest
     * @return bond state <p>Possible values are
     * {@link BluetoothDevice#BOND_NONE},
     * {@link BluetoothDevice#BOND_BONDING},
     * {@link BluetoothDevice#BOND_BONDED}.
     */
    @VisibleForTesting
    public int getBondState(BluetoothDevice device) {
        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
        DeviceProperties deviceProp = mRemoteDevices.getDeviceProperties(device);
        if (deviceProp == null) {
@@ -1923,7 +1938,13 @@ public class AdapterService extends Service {
        return getConnectionStateNative(addr);
    }

    String getRemoteName(BluetoothDevice device) {
    /**
     * Same as API method {@link BluetoothDevice#getName()}
     *
     * @param device remote device of interest
     * @return remote device name
     */
    public String getRemoteName(BluetoothDevice device) {
        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
        if (mRemoteDevices == null) {
            return null;
+7 −4
Original line number Diff line number Diff line
@@ -20,6 +20,7 @@ import android.os.Looper;
import android.util.Log;

import com.android.bluetooth.Utils;
import com.android.bluetooth.btservice.AdapterService;

/**
 * Factory class for object initialization to help with unit testing
@@ -63,15 +64,17 @@ public class HeadsetObjectsFactory {
     *
     * @param device the remote device associated with this state machine
     * @param looper the thread that the state machine is supposed to run on
     * @param service the headset service
     * @param headsetService the headset service
     * @param adapterService the adapter service
     * @param nativeInterface native interface
     * @param systemInterface system interface
     * @return a state machine that is initialized and started, ready to go
     */
    public HeadsetStateMachine makeStateMachine(BluetoothDevice device, Looper looper,
            HeadsetService service, HeadsetNativeInterface nativeInterface,
            HeadsetSystemInterface systemInterface) {
        return HeadsetStateMachine.make(device, looper, service, nativeInterface, systemInterface);
            HeadsetService headsetService, AdapterService adapterService,
            HeadsetNativeInterface nativeInterface, HeadsetSystemInterface systemInterface) {
        return HeadsetStateMachine.make(device, looper, headsetService, adapterService,
                nativeInterface, systemInterface);
    }

    /**
+48 −16
Original line number Diff line number Diff line
@@ -234,10 +234,8 @@ public class HeadsetService extends ProfileService {
     * @param stackEvent event from native stack
     */
    void messageFromNative(HeadsetStackEvent stackEvent) {
        if (stackEvent.device == null) {
            Log.wtfStack(TAG, "messageFromNative, device is null, event: " + stackEvent);
            return;
        }
        Objects.requireNonNull(stackEvent.device,
                "Device should never be null, event: " + stackEvent);
        synchronized (mStateMachines) {
            HeadsetStateMachine stateMachine = mStateMachines.get(stackEvent.device);
            if (stackEvent.type == HeadsetStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED) {
@@ -248,16 +246,17 @@ public class HeadsetService extends ProfileService {
                        if (stateMachine == null) {
                            stateMachine = HeadsetObjectsFactory.getInstance()
                                    .makeStateMachine(stackEvent.device,
                                            mStateMachinesThread.getLooper(), this,
                                            mStateMachinesThread.getLooper(), this, mAdapterService,
                                            mNativeInterface, mSystemInterface);
                            mStateMachines.put(stackEvent.device, stateMachine);
                        }
                        break;
                    }
                }
            } else if (stateMachine == null) {
                Log.wtfStack(TAG, "State machine not found for stack event: " + stackEvent);
                return;
            }
            if (stateMachine == null) {
                throw new IllegalStateException(
                        "State machine not found for stack event: " + stackEvent);
            }
            stateMachine.sendMessage(HeadsetStateMachine.STACK_EVENT, stackEvent);
        }
@@ -315,8 +314,9 @@ public class HeadsetService extends ProfileService {
                case BluetoothDevice.ACTION_BOND_STATE_CHANGED: {
                    int state = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE,
                            BluetoothDevice.ERROR);
                    BluetoothDevice device =
                            intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
                    BluetoothDevice device = Objects.requireNonNull(
                            intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE),
                            "ACTION_BOND_STATE_CHANGED with no EXTRA_DEVICE");
                    logD("Bond state changed for device: " + device + " state: " + state);
                    if (state != BluetoothDevice.BOND_NONE) {
                        break;
@@ -326,9 +326,11 @@ public class HeadsetService extends ProfileService {
                        if (stateMachine == null) {
                            break;
                        }
                        logD("Removing state machine for device: " + device);
                        HeadsetObjectsFactory.getInstance().destroyStateMachine(stateMachine);
                        mStateMachines.remove(device);
                        if (stateMachine.getConnectionState()
                                != BluetoothProfile.STATE_DISCONNECTED) {
                            break;
                        }
                        removeStateMachine(device);
                    }
                    break;
                }
@@ -626,13 +628,18 @@ public class HeadsetService extends ProfileService {
            Log.w(TAG, "connect: PRIORITY_OFF, device=" + device);
            return false;
        }
        ParcelUuid[] featureUuids = mAdapterService.getRemoteUuids(device);
        if (!BluetoothUuid.containsAnyUuid(featureUuids, HEADSET_UUIDS)) {
            Log.e(TAG, "connect: Cannot connect to " + device + ": no headset UUID");
            return false;
        }
        synchronized (mStateMachines) {
            Log.i(TAG, "connect: device=" + device);
            HeadsetStateMachine stateMachine = mStateMachines.get(device);
            if (stateMachine == null) {
                stateMachine = HeadsetObjectsFactory.getInstance()
                        .makeStateMachine(device, mStateMachinesThread.getLooper(), this,
                                mNativeInterface, mSystemInterface);
                                mAdapterService, mNativeInterface, mSystemInterface);
                mStateMachines.put(device, stateMachine);
            }
            int connectionState = stateMachine.getConnectionState();
@@ -699,7 +706,14 @@ public class HeadsetService extends ProfileService {
        return devices;
    }

    private List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
    /**
     * Same as the API method {@link BluetoothHeadset#getDevicesMatchingConnectionStates(int[])}
     *
     * @param states an array of states from {@link BluetoothProfile}
     * @return a list of devices matching the array of connection states
     */
    @VisibleForTesting
    public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
        ArrayList<BluetoothDevice> devices = new ArrayList<>();
        if (states == null) {
@@ -1217,7 +1231,7 @@ public class HeadsetService extends ProfileService {
        // Check priority and accept or reject the connection.
        // Note: Logic can be simplified, but keeping it this way for readability
        int priority = getPriority(device);
        int bondState = device.getBondState();
        int bondState = mAdapterService.getBondState(device);
        // If priority is undefined, it is likely that our SDP has not completed and peer is
        // initiating the connection. Allow this connection only if the device is bonded or bonding
        if ((priority == BluetoothProfile.PRIORITY_UNDEFINED) && (bondState
@@ -1240,6 +1254,24 @@ public class HeadsetService extends ProfileService {
        return true;
    }

    /**
     * Remove state machine in {@link #mStateMachines} for a {@link BluetoothDevice}
     *
     * @param device device whose state machine is to be removed.
     */
    void removeStateMachine(BluetoothDevice device) {
        synchronized (mStateMachines) {
            HeadsetStateMachine stateMachine = mStateMachines.get(device);
            if (stateMachine == null) {
                Log.w(TAG, "removeStateMachine(), " + device + " does not have a state machine");
                return;
            }
            Log.i(TAG, "removeStateMachine(), removing state machine for device: " + device);
            HeadsetObjectsFactory.getInstance().destroyStateMachine(stateMachine);
            mStateMachines.remove(device);
        }
    }

    @Override
    public void dump(StringBuilder sb) {
        synchronized (mStateMachines) {
+45 −32
Original line number Diff line number Diff line
@@ -36,6 +36,7 @@ import android.support.annotation.VisibleForTesting;
import android.telephony.PhoneNumberUtils;
import android.util.Log;

import com.android.bluetooth.btservice.AdapterService;
import com.android.bluetooth.btservice.ProfileService;
import com.android.internal.util.State;
import com.android.internal.util.StateMachine;
@@ -129,7 +130,8 @@ public class HeadsetStateMachine extends StateMachine {
    private HeadsetStateBase mPrevState;

    // Run time dependencies
    private final HeadsetService mService;
    private final HeadsetService mHeadsetService;
    private final AdapterService mAdapterService;
    private final HeadsetNativeInterface mNativeInterface;
    private final HeadsetSystemInterface mSystemInterface;

@@ -170,19 +172,21 @@ public class HeadsetStateMachine extends StateMachine {
        VOICE_COMMAND_INTENT.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    }

    private HeadsetStateMachine(BluetoothDevice device, Looper looper, HeadsetService service,
    private HeadsetStateMachine(BluetoothDevice device, Looper looper,
            HeadsetService headsetService, AdapterService adapterService,
            HeadsetNativeInterface nativeInterface, HeadsetSystemInterface systemInterface) {
        super(TAG, Objects.requireNonNull(looper, "looper cannot be null"));
        // Enable/Disable StateMachine debug logs
        setDbg(DBG);
        mDevice = Objects.requireNonNull(device, "device cannot be null");
        mService = Objects.requireNonNull(service, "service cannot be null");
        mHeadsetService = Objects.requireNonNull(headsetService, "headsetService cannot be null");
        mNativeInterface =
                Objects.requireNonNull(nativeInterface, "nativeInterface cannot be null");
        mSystemInterface =
                Objects.requireNonNull(systemInterface, "systemInterface cannot be null");
        mAdapterService = Objects.requireNonNull(adapterService, "AdapterService cannot be null");
        // Create phonebook helper
        mPhonebook = new AtPhonebook(mService, mNativeInterface);
        mPhonebook = new AtPhonebook(mHeadsetService, mNativeInterface);
        // Initialize state machine
        addState(mDisconnected);
        addState(mConnecting);
@@ -194,12 +198,14 @@ public class HeadsetStateMachine extends StateMachine {
        setInitialState(mDisconnected);
    }

    static HeadsetStateMachine make(BluetoothDevice device, Looper looper, HeadsetService service,
    static HeadsetStateMachine make(BluetoothDevice device, Looper looper,
            HeadsetService headsetService, AdapterService adapterService,
            HeadsetNativeInterface nativeInterface, HeadsetSystemInterface systemInterface) {
        Log.i(TAG, "make");
        HeadsetStateMachine stateMachine =
                new HeadsetStateMachine(device, looper, service, nativeInterface, systemInterface);
                new HeadsetStateMachine(device, looper, headsetService, adapterService,
                        nativeInterface, systemInterface);
        stateMachine.start();
        Log.i(TAG, "Created state machine " + stateMachine + " for " + device);
        return stateMachine;
    }

@@ -302,13 +308,14 @@ public class HeadsetStateMachine extends StateMachine {
                // Headset is disconnecting, stop Virtual call if active.
                terminateScoUsingVirtualVoiceCall();
            }
            mService.onConnectionStateChangedFromStateMachine(device, fromState, toState);
            mHeadsetService.onConnectionStateChangedFromStateMachine(device, fromState, toState);
            Intent intent = new Intent(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
            intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, fromState);
            intent.putExtra(BluetoothProfile.EXTRA_STATE, toState);
            intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
            intent.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
            mService.sendBroadcastAsUser(intent, UserHandle.ALL, HeadsetService.BLUETOOTH_PERM);
            mHeadsetService.sendBroadcastAsUser(intent, UserHandle.ALL,
                    HeadsetService.BLUETOOTH_PERM);
        }

        // Should not be called from enter() method
@@ -319,12 +326,13 @@ public class HeadsetStateMachine extends StateMachine {
                // needs to be cleaned up.So call terminateScoUsingVirtualVoiceCall.
                terminateScoUsingVirtualVoiceCall();
            }
            mService.onAudioStateChangedFromStateMachine(device, fromState, toState);
            mHeadsetService.onAudioStateChangedFromStateMachine(device, fromState, toState);
            Intent intent = new Intent(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED);
            intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, fromState);
            intent.putExtra(BluetoothProfile.EXTRA_STATE, toState);
            intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
            mService.sendBroadcastAsUser(intent, UserHandle.ALL, HeadsetService.BLUETOOTH_PERM);
            mHeadsetService.sendBroadcastAsUser(intent, UserHandle.ALL,
                    HeadsetService.BLUETOOTH_PERM);
        }

        /**
@@ -451,6 +459,11 @@ public class HeadsetStateMachine extends StateMachine {
            mWaitingForVoiceRecognition = false;
            mAudioParams.clear();
            broadcastStateTransitions();
            // Remove the state machine for unbonded devices
            if (mPrevState != null
                    && mAdapterService.getBondState(mDevice) == BluetoothDevice.BOND_NONE) {
                getHandler().post(() -> mHeadsetService.removeStateMachine(mDevice));
            }
        }

        @Override
@@ -516,12 +529,12 @@ public class HeadsetStateMachine extends StateMachine {
                // Both events result in Connecting state as SLC establishment is still required
                case HeadsetHalConstants.CONNECTION_STATE_CONNECTED:
                case HeadsetHalConstants.CONNECTION_STATE_CONNECTING:
                    if (mService.okToAcceptConnection(mDevice)) {
                    if (mHeadsetService.okToAcceptConnection(mDevice)) {
                        stateLogI("accept incoming connection");
                        transitionTo(mConnecting);
                    } else {
                        stateLogI("rejected incoming HF, priority=" + mService.getPriority(mDevice)
                                + " bondState=" + mDevice.getBondState());
                        stateLogI("rejected incoming HF, priority=" + mHeadsetService.getPriority(
                                mDevice) + " bondState=" + mAdapterService.getBondState(mDevice));
                        // Reject the connection and stay in Disconnected state itself
                        if (!mNativeInterface.disconnectHfp(mDevice)) {
                            stateLogE("failed to disconnect");
@@ -1219,8 +1232,8 @@ public class HeadsetStateMachine extends StateMachine {
            // Set active device to current active SCO device when the current active device
            // is different from mCurrentDevice. This is to accommodate active device state
            // mis-match between native and Java.
            if (!mDevice.equals(mService.getActiveDevice())) {
                mService.setActiveDevice(mDevice);
            if (!mDevice.equals(mHeadsetService.getActiveDevice())) {
                mHeadsetService.setActiveDevice(mDevice);
            }
            setAudioParameters();
            mSystemInterface.getAudioManager().setBluetoothScoOn(true);
@@ -1460,7 +1473,7 @@ public class HeadsetStateMachine extends StateMachine {
                    }
                }
                try {
                    mService.startActivity(VOICE_COMMAND_INTENT);
                    mHeadsetService.startActivity(VOICE_COMMAND_INTENT);
                } catch (ActivityNotFoundException e) {
                    mNativeInterface.atResponseCode(device, HeadsetHalConstants.AT_RESPONSE_ERROR,
                            0);
@@ -1549,7 +1562,7 @@ public class HeadsetStateMachine extends StateMachine {

    private synchronized void expectVoiceRecognition(BluetoothDevice device) {
        mWaitingForVoiceRecognition = true;
        mService.setActiveDevice(device);
        mHeadsetService.setActiveDevice(device);
        sendMessageDelayed(START_VR_TIMEOUT, device, START_VR_TIMEOUT_MS);
        if (!mSystemInterface.getVoiceRecognitionWakeLock().isHeld()) {
            mSystemInterface.getVoiceRecognitionWakeLock().acquire(START_VR_TIMEOUT_MS);
@@ -1576,7 +1589,7 @@ public class HeadsetStateMachine extends StateMachine {
        intent.addCategory(BluetoothHeadset.VENDOR_SPECIFIC_HEADSET_EVENT_COMPANY_ID_CATEGORY + "."
                + Integer.toString(companyId));

        mService.sendBroadcastAsUser(intent, UserHandle.ALL, HeadsetService.BLUETOOTH_PERM);
        mHeadsetService.sendBroadcastAsUser(intent, UserHandle.ALL, HeadsetService.BLUETOOTH_PERM);
    }

    private void setAudioParameters() {
@@ -1720,11 +1733,11 @@ public class HeadsetStateMachine extends StateMachine {
        }
        // Check for virtual call to terminate before sending Call Intent
        terminateScoUsingVirtualVoiceCall();
        mService.setActiveDevice(mDevice);
        mHeadsetService.setActiveDevice(mDevice);
        Intent intent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
                Uri.fromParts(SCHEME_TEL, dialNumber, null));
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        mService.startActivity(intent);
        mHeadsetService.startActivity(intent);
        // TODO(BT) continue send OK reults code after call starts
        //          hold wait lock, start a timer, set wait call flag
        //          Get call started indication from bluetooth phone
@@ -1761,7 +1774,7 @@ public class HeadsetStateMachine extends StateMachine {
                if (!hasMessages(DIALING_OUT_TIMEOUT)) {
                    return;
                }
                mService.setActiveDevice(mDevice);
                mHeadsetService.setActiveDevice(mDevice);
                mNativeInterface.atResponseCode(mDevice, HeadsetHalConstants.AT_RESPONSE_OK, 0);
                removeMessages(DIALING_OUT_TIMEOUT);
            } else if (callState.mCallState == HeadsetHalConstants.CALL_STATE_ACTIVE
@@ -2063,7 +2076,7 @@ public class HeadsetStateMachine extends StateMachine {
            mSystemInterface.answerCall(device);
        } else if (phoneState.getNumActiveCall() > 0) {
            if (getAudioState() != BluetoothHeadset.STATE_AUDIO_DISCONNECTED) {
                mService.setActiveDevice(mDevice);
                mHeadsetService.setActiveDevice(mDevice);
                mNativeInterface.connectAudio(mDevice);
            } else {
                mSystemInterface.hangupCall(device, false);
@@ -2074,11 +2087,11 @@ public class HeadsetStateMachine extends StateMachine {
                log("processKeyPressed, last dial number null");
                return;
            }
            mService.setActiveDevice(mDevice);
            mHeadsetService.setActiveDevice(mDevice);
            Intent intent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
                    Uri.fromParts(SCHEME_TEL, dialNumber, null));
            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            mService.startActivity(intent);
            mHeadsetService.startActivity(intent);
        }
    }

@@ -2095,7 +2108,7 @@ public class HeadsetStateMachine extends StateMachine {
        intent.putExtra(BluetoothHeadset.EXTRA_HF_INDICATORS_IND_ID, indId);
        intent.putExtra(BluetoothHeadset.EXTRA_HF_INDICATORS_IND_VALUE, indValue);

        mService.sendBroadcast(intent, HeadsetService.BLUETOOTH_PERM);
        mHeadsetService.sendBroadcast(intent, HeadsetService.BLUETOOTH_PERM);
    }

    private void processAtBind(String atString, BluetoothDevice device) {
@@ -2159,7 +2172,7 @@ public class HeadsetStateMachine extends StateMachine {
    }

    private String getCurrentDeviceName() {
        String deviceName = mDevice.getName();
        String deviceName = mAdapterService.getRemoteName(mDevice);
        if (deviceName == null) {
            return "<unknown>";
        }
@@ -2169,28 +2182,28 @@ public class HeadsetStateMachine extends StateMachine {
    // Accept incoming SCO only when there is in-band ringing, incoming call,
    // active call, VR activated, active VOIP call
    private boolean isScoAcceptable() {
        if (mService.getForceScoAudio()) {
        if (mHeadsetService.getForceScoAudio()) {
            return true;
        }
        BluetoothDevice activeDevice = mService.getActiveDevice();
        BluetoothDevice activeDevice = mHeadsetService.getActiveDevice();
        if (!mDevice.equals(activeDevice)) {
            Log.w(TAG, "isScoAcceptable: rejected SCO since " + mDevice
                    + " is not the current active device " + activeDevice);
            return false;
        }
        if (!mService.getAudioRouteAllowed()) {
        if (!mHeadsetService.getAudioRouteAllowed()) {
            Log.w(TAG, "isScoAcceptabl: rejected SCO since audio route is not allowed");
            return false;
        }
        if (mSystemInterface.isInCall() || mVoiceRecognitionStarted) {
            return true;
        }
        if (mSystemInterface.isRinging() && mService.isInbandRingingEnabled()) {
        if (mSystemInterface.isRinging() && mHeadsetService.isInbandRingingEnabled()) {
            return true;
        }
        Log.w(TAG, "isScoAcceptable: rejected SCO, inCall=" + mSystemInterface.isInCall()
                + ", voiceRecognition=" + mVoiceRecognitionStarted + ", ringing=" + mSystemInterface
                .isRinging() + ", inbandRinging=" + mService.isInbandRingingEnabled());
                .isRinging() + ", inbandRinging=" + mHeadsetService.isInbandRingingEnabled());
        return false;
    }

+341 −0

File added.

Preview size limit exceeded, changes collapsed.

Loading